~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2005-08-30 18:22:25 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050830182225-e650f373870c640f
Added committer to revisions

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron@aaronbentley.com>
2
 
# Copyright (C) 2007 John Arbash Meinel
 
1
# Copyright (C) 2005 Aaron Bentley
 
2
# <aaron.bentley@utoronto.ca>
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 codecs
18
 
import errno
 
17
import bzrlib
19
18
import os
20
 
import re
 
19
import os.path
 
20
import sys
21
21
import tempfile
22
22
import shutil
23
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():
 
24
 
 
25
def temp_branch():
44
26
    dirname = tempfile.mkdtemp("temp-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):
 
27
    return bzrlib.Branch(dirname, init=True)
 
28
 
 
29
def rm_branch(br):
 
30
    shutil.rmtree(br.base)
 
31
 
 
32
def is_clean(cur_branch):
51
33
    """
52
34
    Return true if no files are modifed or unknown
 
35
    >>> br = temp_branch()
 
36
    >>> is_clean(br)
 
37
    True
 
38
    >>> fooname = os.path.join(br.base, "foo")
 
39
    >>> file(fooname, "wb").write("bar")
 
40
    >>> is_clean(br)
 
41
    False
 
42
    >>> bzrlib.add.smart_add([fooname])
 
43
    >>> is_clean(br)
 
44
    False
 
45
    >>> br.commit("added file")
 
46
    >>> is_clean(br)
 
47
    True
 
48
    >>> rm_branch(br)
53
49
    """
54
 
    old_tree = cur_tree.basis_tree()
55
 
    new_tree = cur_tree
 
50
    from bzrlib.diff import compare_trees
 
51
    old_tree = cur_branch.basis_tree()
 
52
    new_tree = cur_branch.working_tree()
56
53
    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
 
54
    for path, file_class, kind, file_id in new_tree.list_files():
 
55
        if file_class in ('?', 'I'):
 
56
            non_source.append(path)
 
57
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
 
58
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
 
59
        len(delta.modified) > 0:
 
60
        return False, non_source
 
61
    return True, non_source 
 
62
 
 
63
def set_pull_data(br, location, rev_id):
 
64
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
 
65
    pull_file.write("%s\n%s\n" % (location, rev_id))
 
66
 
 
67
def get_pull_data(br):
 
68
    """
 
69
    >>> br = temp_branch()
 
70
    >>> get_pull_data(br)
 
71
    (None, None)
 
72
    >>> set_pull_data(br, 'http://somewhere', '888-777')
 
73
    >>> get_pull_data(br)
 
74
    ('http://somewhere', '888-777')
 
75
    >>> rm_branch(br)
 
76
    """
 
77
    filename = br.controlfilename("x-pull-data")
 
78
    if not os.path.exists(filename):
 
79
        return (None, None)
 
80
    pull_file = file (filename, "rb")
 
81
    location, rev_id = [f.rstrip('\n') for f in pull_file]
 
82
    return location, rev_id
 
83
 
 
84
def set_push_data(br, location):
 
85
    push_file = file (br.controlfilename("x-push-data"), "wb")
 
86
    push_file.write("%s\n" % location)
 
87
 
 
88
def get_push_data(br):
 
89
    """
 
90
    >>> br = temp_branch()
 
91
    >>> get_push_data(br) is None
74
92
    True
75
 
    >>> set_push_data(tree, 'http://somewhere')
76
 
    >>> get_push_data(tree)
77
 
    u'http://somewhere'
78
 
    >>> rm_tree(tree)
 
93
    >>> set_push_data(br, 'http://somewhere')
 
94
    >>> get_push_data(br)
 
95
    'http://somewhere'
 
96
    >>> rm_branch(br)
79
97
    """
80
 
    try:
81
 
        location = tree.branch.control_files.get_utf8('x-push-data').read()
82
 
    except NoSuchFile:
 
98
    filename = br.controlfilename("x-push-data")
 
99
    if not os.path.exists(filename):
83
100
        return None
84
 
    return location.rstrip('\n')
 
101
    push_file = file (filename, "rb")
 
102
    (location,) = [f.rstrip('\n') for f in push_file]
 
103
    return location
85
104
 
86
105
"""
87
106
>>> shell_escape('hello')
101
120
    arg_str = " ".join([shell_escape(a) for a in args])
102
121
    return os.system(arg_str)
103
122
 
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"]
 
123
def rsync(source, target, ssh=False, excludes=()):
 
124
    """
 
125
    >>> real_system = os.system
 
126
    >>> os.system = sys.stdout.write
 
127
    >>> rsync("a", "b")
 
128
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e \\a \\b
 
129
    >>> rsync("a", "b", excludes=("*.py",))
 
130
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e\
 
131
 \\-\\-\\e\\x\\c\\l\\u\\d\\e \\*\\.\\p\\y \\a \\b
 
132
    >>> os.system = real_system
 
133
    """
 
134
    cmd = ["rsync", "-av", "--delete"]
116
135
    if ssh:
117
136
        cmd.extend(('-e', 'ssh'))
118
137
    if len(excludes) > 0:
119
138
        cmd.extend(('--exclude-from', '-'))
120
139
    cmd.extend((source, target))
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
 
 
 
140
    proc = Popen(cmd, stdin=PIPE)
133
141
    proc.stdin.write('\n'.join(excludes)+'\n')
134
142
    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
 
 
 
143
    return proc.wait()
 
144
 
 
145
exclusions = ('.bzr/x-push-data', '.bzr/x-pull-data', '.bzr/stat-cache')
 
146
 
 
147
 
 
148
def push(cur_branch, location=None):
 
149
    push_location = get_push_data(cur_branch)
 
150
    if location is not None:
 
151
        if not location.endswith('/'):
 
152
            location += '/'
 
153
        push_location = location
 
154
    
 
155
    if push_location is None:
 
156
        print "No push location saved.  Please specify one on the command line."
 
157
        sys.exit(1)
 
158
 
 
159
    clean, non_source = is_clean(cur_branch)
 
160
    if not clean:
 
161
        print """Error: This tree has uncommitted changes or unknown (?) files.
 
162
Use "bzr status" to list them."""
 
163
        sys.exit(1)
 
164
    non_source.extend(exclusions)
 
165
 
 
166
    print "Pushing to %s" % push_location
 
167
    rsync(cur_branch.base+'/', push_location, ssh=True, excludes=non_source)
 
168
 
 
169
    set_push_data(cur_branch, push_location)
412
170
 
413
171
def run_tests():
414
172
    import doctest