~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2009-11-11 07:34:31 UTC
  • mfrom: (730.1.2 2.0)
  • Revision ID: aaron@aaronbentley.com-20091111073431-sp3sasbae3zm5l5e
Merged 2.0 into bzrtools.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
 
1
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron@aaronbentley.com>
 
2
# Copyright (C) 2007 John Arbash Meinel
3
3
#
4
4
#    This program is free software; you can redistribute it and/or modify
5
5
#    it under the terms of the GNU General Public License as published by
14
14
#    You should have received a copy of the GNU General Public License
15
15
#    along with this program; if not, write to the Free Software
16
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
import bzrlib
 
17
import codecs
 
18
import errno
18
19
import os
19
 
import os.path
20
 
import sys
 
20
import re
21
21
import tempfile
22
22
import shutil
23
 
 
24
 
def temp_branch():
 
23
from subprocess import Popen, PIPE
 
24
import sys
 
25
 
 
26
import bzrlib
 
27
from bzrlib import revision as _mod_revision, trace, urlutils
 
28
import bzrlib.errors
 
29
from bzrlib.errors import (
 
30
    BzrCommandError,
 
31
    BzrError,
 
32
    ConnectionError,
 
33
    NotBranchError,
 
34
    NoSuchFile,
 
35
    NoWorkingTree,
 
36
    PermissionDenied,
 
37
    UnsupportedFormatError,
 
38
    TransportError,
 
39
    )
 
40
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
41
from bzrlib.transport import get_transport
 
42
 
 
43
def temp_tree():
25
44
    dirname = tempfile.mkdtemp("temp-branch")
26
 
    return bzrlib.Branch(dirname, init=True)
27
 
 
28
 
def rm_branch(br):
29
 
    shutil.rmtree(br.base)
30
 
 
31
 
def is_clean(cur_branch):
 
45
    return BzrDir.create_standalone_workingtree(dirname)
 
46
 
 
47
def rm_tree(tree):
 
48
    shutil.rmtree(tree.basedir)
 
49
 
 
50
def is_clean(cur_tree):
32
51
    """
33
52
    Return true if no files are modifed or unknown
34
 
    >>> br = temp_branch()
35
 
    >>> is_clean(br)
36
 
    True
37
 
    >>> fooname = os.path.join(br.base, "foo")
38
 
    >>> file(fooname, "wb").write("bar")
39
 
    >>> is_clean(br)
40
 
    False
41
 
    >>> bzrlib.add.smart_add([fooname])
42
 
    >>> is_clean(br)
43
 
    False
44
 
    >>> br.commit("added file")
45
 
    >>> is_clean(br)
46
 
    True
47
 
    >>> rm_branch(br)
48
 
    """
49
 
    from bzrlib.diff import compare_trees
50
 
    old_tree = cur_branch.basis_tree()
51
 
    new_tree = cur_branch.working_tree()
52
 
    for path, file_class, kind, file_id in new_tree.list_files():
53
 
        if file_class == '?':
54
 
            return False
55
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
56
 
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
57
 
        len(delta.modified) > 0:
58
 
        return False
59
 
    return True
60
 
 
61
 
def set_pull_data(br, location, rev_id):
62
 
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
63
 
    pull_file.write("%s\n%s\n" % (location, rev_id))
64
 
 
65
 
def get_pull_data(br):
66
 
    """
67
 
    >>> br = temp_branch()
68
 
    >>> get_pull_data(br)
69
 
    (None, None)
70
 
    >>> set_pull_data(br, 'http://somewhere', '888-777')
71
 
    >>> get_pull_data(br)
72
 
    ('http://somewhere', '888-777')
73
 
    >>> rm_branch(br)
74
 
    """
75
 
    filename = br.controlfilename("x-pull-data")
76
 
    if not os.path.exists(filename):
77
 
        return (None, None)
78
 
    pull_file = file (filename, "rb")
79
 
    location, rev_id = [f.rstrip('\n') for f in pull_file]
80
 
    return location, rev_id
81
 
 
82
 
def set_push_data(br, location):
83
 
    push_file = file (br.controlfilename("x-push-data"), "wb")
84
 
    push_file.write("%s\n" % location)
85
 
 
86
 
def get_push_data(br):
87
 
    """
88
 
    >>> br = temp_branch()
89
 
    >>> get_push_data(br) is None
90
 
    True
91
 
    >>> set_push_data(br, 'http://somewhere')
92
 
    >>> get_push_data(br)
93
 
    'http://somewhere'
94
 
    >>> rm_branch(br)
95
 
    """
96
 
    filename = br.controlfilename("x-push-data")
97
 
    if not os.path.exists(filename):
 
53
    """
 
54
    old_tree = cur_tree.basis_tree()
 
55
    new_tree = cur_tree
 
56
    non_source = []
 
57
    new_tree.lock_read()
 
58
    try:
 
59
        for path, file_class, kind, file_id, entry in new_tree.list_files():
 
60
            if file_class in ('?', 'I'):
 
61
                non_source.append(path)
 
62
        delta = new_tree.changes_from(old_tree, want_unchanged=False)
 
63
    finally:
 
64
        new_tree.unlock()
 
65
    return not delta.has_changed(), non_source
 
66
 
 
67
def set_push_data(tree, location):
 
68
    tree.branch._transport.put_bytes("x-push-data", "%s\n" % location)
 
69
 
 
70
def get_push_data(tree):
 
71
    """
 
72
    >>> tree = temp_tree()
 
73
    >>> get_push_data(tree) is None
 
74
    True
 
75
    >>> set_push_data(tree, 'http://somewhere')
 
76
    >>> get_push_data(tree)
 
77
    u'http://somewhere'
 
78
    >>> rm_tree(tree)
 
79
    """
 
80
    try:
 
81
        location = tree.branch._transport.get('x-push-data').read()
 
82
    except NoSuchFile:
98
83
        return None
99
 
    push_file = file (filename, "rb")
100
 
    (location,) = [f.rstrip('\n') for f in push_file]
101
 
    return location
 
84
    location = location.decode('utf-8')
 
85
    return location.rstrip('\n')
102
86
 
103
87
"""
104
88
>>> shell_escape('hello')
118
102
    arg_str = " ".join([shell_escape(a) for a in args])
119
103
    return os.system(arg_str)
120
104
 
121
 
def rsync(source, target, ssh=False, exclude_globs=()):
122
 
    """
123
 
    >>> real_system = os.system
124
 
    >>> os.system = sys.stdout.write
125
 
    >>> rsync("a", "b")
126
 
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e \\a \\b
127
 
    >>> rsync("a", "b", exclude_globs=("*.py",))
128
 
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e\
129
 
 \\-\\-\\e\\x\\c\\l\\u\\d\\e \\*\\.\\p\\y \\a \\b
130
 
    >>> os.system = real_system
131
 
    """
132
 
    cmd = ["rsync", "-av", "--delete"]
 
105
class RsyncUnknownStatus(Exception):
 
106
    def __init__(self, status):
 
107
        Exception.__init__(self, "Unknown status: %d" % status)
 
108
 
 
109
class NoRsync(Exception):
 
110
    def __init__(self, rsync_name):
 
111
        Exception.__init__(self, "%s not found." % rsync_name)
 
112
 
 
113
 
 
114
def rsync(source, target, ssh=False, excludes=(), silent=False,
 
115
          rsync_name="rsync"):
 
116
    cmd = [rsync_name, "-av", "--delete"]
133
117
    if ssh:
134
118
        cmd.extend(('-e', 'ssh'))
135
 
    for exclude in exclude_globs:
136
 
        cmd.extend(('--exclude', exclude))
 
119
    if len(excludes) > 0:
 
120
        cmd.extend(('--exclude-from', '-'))
137
121
    cmd.extend((source, target))
138
 
    safe_system(cmd)
139
 
 
140
 
exclusions = ('x-push-data', 'x-pull-data')
141
 
 
142
 
 
143
 
def pull(cur_branch, location=None, overwrite=False):
144
 
    pull_location, pull_revision = get_pull_data(cur_branch)
145
 
    if pull_location is not None:
146
 
        if not overwrite and cur_branch.last_patch() != pull_revision:
147
 
            print "Aborting: This branch has had commits, so pull would lose data."
148
 
            sys.exit(1)
149
 
    if location is not None:
150
 
        pull_location = location
151
 
        if not pull_location.endswith('/'):
152
 
            pull_location+='/'
153
 
 
154
 
    if pull_location is None:
155
 
        print "No pull location saved.  Please specify one on the command line."
156
 
        sys.exit(1)
157
 
 
158
 
    if not is_clean(cur_branch):
159
 
        print "Error: This tree has uncommitted changes or unknown (?) files."
160
 
        sys.exit(1)
161
 
 
162
 
    print "Synchronizing with %s" % pull_location
163
 
    rsync (pull_location, cur_branch.base+'/', exclude_globs=exclusions)
164
 
 
165
 
    set_pull_data(cur_branch, pull_location, cur_branch.last_patch())
166
 
 
167
 
 
168
 
def push(cur_branch, location=None):
169
 
    push_location = get_push_data(cur_branch)
170
 
    if location is not None:
171
 
        if not location.endswith('/'):
172
 
            location += '/'
173
 
        push_location = location
174
 
    
175
 
    if push_location is None:
176
 
        print "No push location saved.  Please specify one on the command line."
177
 
        sys.exit(1)
178
 
 
179
 
    if not is_clean(cur_branch):
180
 
        print """Error: This tree has uncommitted changes or unknown (?) files.
181
 
Use "bzr status" to list them."""
182
 
        sys.exit(1)
183
 
 
184
 
    print "Pushing to %s" % push_location
185
 
    rsync(cur_branch.base+'/', push_location, ssh=True,
186
 
          exclude_globs=exclusions)
187
 
 
188
 
    set_push_data(cur_branch, push_location)
 
122
    if silent:
 
123
        stderr = PIPE
 
124
        stdout = PIPE
 
125
    else:
 
126
        stderr = None
 
127
        stdout = None
 
128
    try:
 
129
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
130
    except OSError, e:
 
131
        if e.errno == errno.ENOENT:
 
132
            raise NoRsync(rsync_name)
 
133
 
 
134
    proc.stdin.write('\n'.join(excludes)+'\n')
 
135
    proc.stdin.close()
 
136
    if silent:
 
137
        proc.stderr.read()
 
138
        proc.stderr.close()
 
139
        proc.stdout.read()
 
140
        proc.stdout.close()
 
141
    proc.wait()
 
142
    if proc.returncode == 12:
 
143
        raise RsyncStreamIO()
 
144
    elif proc.returncode == 23:
 
145
        raise RsyncNoFile(source)
 
146
    elif proc.returncode != 0:
 
147
        raise RsyncUnknownStatus(proc.returncode)
 
148
    return cmd
 
149
 
 
150
 
 
151
def rsync_ls(source, ssh=False, silent=True):
 
152
    cmd = ["rsync"]
 
153
    if ssh:
 
154
        cmd.extend(('-e', 'ssh'))
 
155
    cmd.append(source)
 
156
    if silent:
 
157
        stderr = PIPE
 
158
    else:
 
159
        stderr = None
 
160
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
161
    result = proc.stdout.read()
 
162
    proc.stdout.close()
 
163
    if silent:
 
164
        proc.stderr.read()
 
165
        proc.stderr.close()
 
166
    proc.wait()
 
167
    if proc.returncode == 12:
 
168
        raise RsyncStreamIO()
 
169
    elif proc.returncode == 23:
 
170
        raise RsyncNoFile(source)
 
171
    elif proc.returncode != 0:
 
172
        raise RsyncUnknownStatus(proc.returncode)
 
173
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
174
 
 
175
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
 
176
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
 
177
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
 
178
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
 
179
 
 
180
 
 
181
def read_revision_history(fname):
 
182
    return [l.rstrip('\r\n') for l in
 
183
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
184
 
 
185
 
 
186
def read_revision_info(path):
 
187
    """Parse a last_revision file to determine revision_info"""
 
188
    line = open(path, 'rb').readlines()[0].strip('\n')
 
189
    revno, revision_id = line.split(' ', 1)
 
190
    revno = int(revno)
 
191
    return revno, revision_id
 
192
 
 
193
 
 
194
class RsyncNoFile(Exception):
 
195
    def __init__(self, path):
 
196
        Exception.__init__(self, "No such file %s" % path)
 
197
 
 
198
class RsyncStreamIO(Exception):
 
199
    def __init__(self):
 
200
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
201
 
 
202
 
 
203
class NotStandalone(BzrError):
 
204
 
 
205
    _fmt = '%(location)s is not a standalone tree.'
 
206
    _internal = False
 
207
 
 
208
    def __init__(self, location):
 
209
        BzrError.__init__(self, location=location)
 
210
 
 
211
 
 
212
def get_revision_history(location, _rsync):
 
213
    tempdir = tempfile.mkdtemp('push')
 
214
    my_rsync = _rsync
 
215
    if my_rsync is None:
 
216
        my_rsync = rsync
 
217
    try:
 
218
        history_fname = os.path.join(tempdir, 'revision-history')
 
219
        try:
 
220
            cmd = my_rsync(location+'.bzr/revision-history', history_fname,
 
221
                        silent=True)
 
222
        except RsyncNoFile:
 
223
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
 
224
                        silent=True)
 
225
        history = read_revision_history(history_fname)
 
226
    finally:
 
227
        shutil.rmtree(tempdir)
 
228
    return history
 
229
 
 
230
 
 
231
def get_revision_info(location, _rsync):
 
232
    """Get the revsision_info for an rsync-able branch"""
 
233
    tempdir = tempfile.mkdtemp('push')
 
234
    my_rsync = _rsync
 
235
    if my_rsync is None:
 
236
        my_rsync = rsync
 
237
    try:
 
238
        info_fname = os.path.join(tempdir, 'last-revision')
 
239
        cmd = rsync(location+'.bzr/branch/last-revision', info_fname,
 
240
                    silent=True)
 
241
        return read_revision_info(info_fname)
 
242
    finally:
 
243
        shutil.rmtree(tempdir)
 
244
 
 
245
 
 
246
def history_subset(location, branch, _rsync=None):
 
247
    local_history = branch.revision_history()
 
248
    try:
 
249
        remote_history = get_revision_history(location, _rsync)
 
250
    except RsyncNoFile:
 
251
        revno, revision_id = get_revision_info(location, _rsync)
 
252
        if revision_id == _mod_revision.NULL_REVISION:
 
253
            return True
 
254
        return bool(revision_id.decode('utf-8') in local_history)
 
255
    else:
 
256
        if len(remote_history) > len(local_history):
 
257
            return False
 
258
        for local, remote in zip(remote_history, local_history):
 
259
            if local != remote:
 
260
                return False
 
261
        return True
 
262
 
 
263
 
 
264
def empty_or_absent(location):
 
265
    try:
 
266
        files = rsync_ls(location)
 
267
        return files == ['.']
 
268
    except RsyncNoFile:
 
269
        return True
 
270
 
 
271
def rspush(tree, location=None, overwrite=False, working_tree=True,
 
272
    _rsync=None):
 
273
    tree.lock_write()
 
274
    try:
 
275
        my_rsync = _rsync
 
276
        if my_rsync is None:
 
277
            my_rsync = rsync
 
278
        if (tree.bzrdir.root_transport.base !=
 
279
            tree.branch.bzrdir.root_transport.base):
 
280
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
281
        if (tree.branch.get_bound_location() is not None):
 
282
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
283
        if (tree.branch.repository.is_shared()):
 
284
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
285
        push_location = get_push_data(tree)
 
286
        if location is not None:
 
287
            if not location.endswith('/'):
 
288
                location += '/'
 
289
            push_location = location
 
290
 
 
291
        if push_location is None:
 
292
            raise BzrCommandError("No rspush location known or specified.")
 
293
 
 
294
        if (push_location.find('::') != -1):
 
295
            usessh=False
 
296
        else:
 
297
            usessh=True
 
298
 
 
299
        if (push_location.find('://') != -1 or
 
300
            push_location.find(':') == -1):
 
301
            raise BzrCommandError("Invalid rsync path %r." % push_location)
 
302
 
 
303
        if working_tree:
 
304
            clean, non_source = is_clean(tree)
 
305
            if not clean:
 
306
                raise bzrlib.errors.BzrCommandError(
 
307
                    'This tree has uncommitted changes or unknown'
 
308
                    ' (?) files.  Use "bzr status" to list them.')
 
309
                sys.exit(1)
 
310
            final_exclusions = non_source[:]
 
311
        else:
 
312
            wt = tree
 
313
            final_exclusions = []
 
314
            for path, status, kind, file_id, entry in wt.list_files():
 
315
                final_exclusions.append(path)
 
316
 
 
317
        final_exclusions.extend(exclusions)
 
318
        if not overwrite:
 
319
            try:
 
320
                if not history_subset(push_location, tree.branch,
 
321
                                      _rsync=my_rsync):
 
322
                    raise bzrlib.errors.BzrCommandError(
 
323
                        "Local branch is not a newer version of remote"
 
324
                        " branch.")
 
325
            except RsyncNoFile:
 
326
                if not empty_or_absent(push_location):
 
327
                    raise bzrlib.errors.BzrCommandError(
 
328
                        "Remote location is not a bzr branch (or empty"
 
329
                        " directory)")
 
330
            except RsyncStreamIO:
 
331
                raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
332
                    " specified location.  Please ensure that"
 
333
                    ' "%s" is of the form "machine:/path".' % push_location)
 
334
        trace.note("Pushing to %s", push_location)
 
335
        my_rsync(tree.basedir+'/', push_location, ssh=usessh,
 
336
                 excludes=final_exclusions)
 
337
 
 
338
        set_push_data(tree, push_location)
 
339
    finally:
 
340
        tree.unlock()
 
341
 
 
342
 
 
343
def short_committer(committer):
 
344
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
345
    if len(new_committer) < 2:
 
346
        return committer
 
347
    return new_committer
 
348
 
 
349
 
 
350
def apache_ls(t):
 
351
    """Screen-scrape Apache listings"""
 
352
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
 
353
        ' <a href="'
 
354
    t = t.clone()
 
355
    t._remote_path = lambda x: t.base
 
356
    try:
 
357
        lines = t.get('')
 
358
    except bzrlib.errors.NoSuchFile:
 
359
        return
 
360
    expr = re.compile('<a[^>]*href="([^>]*)\/"[^>]*>', flags=re.I)
 
361
    for line in lines:
 
362
        match = expr.search(line)
 
363
        if match is None:
 
364
            continue
 
365
        url = match.group(1)
 
366
        if url.startswith('http://') or url.startswith('/') or '../' in url:
 
367
            continue
 
368
        if '?' in url:
 
369
            continue
 
370
        yield url.rstrip('/')
 
371
 
 
372
 
 
373
def list_branches(t):
 
374
    def is_inside(branch):
 
375
        return bool(branch.base.startswith(t.base))
 
376
 
 
377
    if t.base.startswith('http://'):
 
378
        def evaluate(bzrdir):
 
379
            try:
 
380
                branch = bzrdir.open_branch()
 
381
                if is_inside(branch):
 
382
                    return True, branch
 
383
                else:
 
384
                    return True, None
 
385
            except NotBranchError:
 
386
                return True, None
 
387
        return [b for b in BzrDir.find_bzrdirs(t, list_current=apache_ls,
 
388
                evaluate=evaluate) if b is not None]
 
389
    elif not t.listable():
 
390
        raise BzrCommandError("Can't list this type of location.")
 
391
    return [b for b in BzrDir.find_branches(t) if is_inside(b)]
 
392
 
 
393
 
 
394
def evaluate_branch_tree(bzrdir):
 
395
    try:
 
396
        tree, branch = bzrdir._get_tree_branch()
 
397
    except NotBranchError:
 
398
        return True, None
 
399
    else:
 
400
        return True, (branch, tree)
 
401
 
 
402
 
 
403
def iter_branch_tree(t, lister=None):
 
404
    return (x for x in BzrDir.find_bzrdirs(t, evaluate=evaluate_branch_tree,
 
405
            list_current=lister) if x is not None)
 
406
 
 
407
 
 
408
def open_from_url(location):
 
409
    location = urlutils.normalize_url(location)
 
410
    dirname, basename = urlutils.split(location)
 
411
    if location.endswith('/') and not basename.endswith('/'):
 
412
        basename += '/'
 
413
    return get_transport(dirname).get(basename)
 
414
 
189
415
 
190
416
def run_tests():
191
417
    import doctest