~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2008-01-11 03:01:54 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20080111030154-apm50v0b0tu93prh
Support branch6 formats in rspush

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