~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2008-11-11 15:55:32 UTC
  • Revision ID: aaron@aaronbentley.com-20081111155532-pjxa3kib17yggl9x
Update shelf tests to new command names

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
18
 
import bzrlib.errors
 
17
import codecs
 
18
import errno
19
19
import os
20
 
import os.path
21
 
import sys
 
20
import re
22
21
import tempfile
23
22
import shutil
24
 
import errno
25
23
from subprocess import Popen, PIPE
26
 
import codecs
27
 
 
28
 
def temp_branch():
 
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():
29
44
    dirname = tempfile.mkdtemp("temp-branch")
30
 
    return bzrlib.branch.Branch.initialize(dirname)
31
 
 
32
 
def rm_branch(br):
33
 
    shutil.rmtree(br.base)
34
 
 
35
 
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):
36
51
    """
37
52
    Return true if no files are modifed or unknown
38
 
    >>> import bzrlib.add
39
 
    >>> br = temp_branch()
40
 
    >>> is_clean(br)
41
 
    (True, [])
42
 
    >>> fooname = os.path.join(br.base, "foo")
43
 
    >>> file(fooname, "wb").write("bar")
44
 
    >>> is_clean(br)
45
 
    (True, ['foo'])
46
 
    >>> bzrlib.add.smart_add_branch(br, [br.base])
47
 
    1
48
 
    >>> is_clean(br)
49
 
    (False, [])
50
 
    >>> br.commit("added file")
51
 
    added foo
52
 
    >>> is_clean(br)
53
 
    (True, [])
54
 
    >>> rm_branch(br)
55
53
    """
56
 
    from bzrlib.diff import compare_trees
57
 
    old_tree = cur_branch.basis_tree()
58
 
    new_tree = cur_branch.working_tree()
 
54
    old_tree = cur_tree.basis_tree()
 
55
    new_tree = cur_tree
59
56
    non_source = []
60
 
    for path, file_class, kind, file_id in new_tree.list_files():
61
 
        if file_class in ('?', 'I'):
62
 
            non_source.append(path)
63
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
64
 
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
65
 
        len(delta.modified) > 0:
66
 
        return False, non_source
67
 
    return True, non_source 
68
 
 
69
 
def set_pull_data(br, location, rev_id):
70
 
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
71
 
    pull_file.write("%s\n%s\n" % (location, rev_id))
72
 
 
73
 
def get_pull_data(br):
74
 
    """
75
 
    >>> br = temp_branch()
76
 
    >>> get_pull_data(br)
77
 
    (None, None)
78
 
    >>> set_pull_data(br, 'http://somewhere', '888-777')
79
 
    >>> get_pull_data(br)
80
 
    ('http://somewhere', '888-777')
81
 
    >>> rm_branch(br)
82
 
    """
83
 
    filename = br.controlfilename("x-pull-data")
84
 
    if not os.path.exists(filename):
85
 
        return (None, None)
86
 
    pull_file = file (filename, "rb")
87
 
    location, rev_id = [f.rstrip('\n') for f in pull_file]
88
 
    return location, rev_id
89
 
 
90
 
def set_push_data(br, location):
91
 
    push_file = file (br.controlfilename("x-push-data"), "wb")
92
 
    push_file.write("%s\n" % location)
93
 
 
94
 
def get_push_data(br):
95
 
    """
96
 
    >>> br = temp_branch()
97
 
    >>> get_push_data(br) is None
 
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
98
74
    True
99
 
    >>> set_push_data(br, 'http://somewhere')
100
 
    >>> get_push_data(br)
101
 
    'http://somewhere'
102
 
    >>> rm_branch(br)
 
75
    >>> set_push_data(tree, 'http://somewhere')
 
76
    >>> get_push_data(tree)
 
77
    u'http://somewhere'
 
78
    >>> rm_tree(tree)
103
79
    """
104
 
    filename = br.controlfilename("x-push-data")
105
 
    if not os.path.exists(filename):
 
80
    try:
 
81
        location = tree.branch._transport.get('x-push-data').read()
 
82
    except NoSuchFile:
106
83
        return None
107
 
    push_file = file (filename, "rb")
108
 
    (location,) = [f.rstrip('\n') for f in push_file]
109
 
    return location
 
84
    location = location.decode('utf-8')
 
85
    return location.rstrip('\n')
110
86
 
111
87
"""
112
88
>>> shell_escape('hello')
134
110
    def __init__(self, rsync_name):
135
111
        Exception.__init__(self, "%s not found." % rsync_name)
136
112
 
137
 
def rsync(source, target, ssh=False, excludes=(), silent=False, 
 
113
 
 
114
def rsync(source, target, ssh=False, excludes=(), silent=False,
138
115
          rsync_name="rsync"):
139
 
    """
140
 
    >>> rsync("a", "b", silent=True)
141
 
    Traceback (most recent call last):
142
 
    RsyncNoFile: No such file a
143
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True)
144
 
    Traceback (most recent call last):
145
 
    RsyncNoFile: No such file a
146
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
147
 
    Traceback (most recent call last):
148
 
    NoRsync: rsyncc not found.
149
 
    """
150
116
    cmd = [rsync_name, "-av", "--delete"]
151
117
    if ssh:
152
118
        cmd.extend(('-e', 'ssh'))
164
130
    except OSError, e:
165
131
        if e.errno == errno.ENOENT:
166
132
            raise NoRsync(rsync_name)
167
 
            
 
133
 
168
134
    proc.stdin.write('\n'.join(excludes)+'\n')
169
135
    proc.stdin.close()
170
136
    if silent:
206
172
        raise RsyncUnknownStatus(proc.returncode)
207
173
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
208
174
 
209
 
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
210
 
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
211
 
              '.bzr/x-rsync-data')
 
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')
212
179
 
213
180
 
214
181
def read_revision_history(fname):
215
182
    return [l.rstrip('\r\n') for l in
216
183
            codecs.open(fname, 'rb', 'utf-8').readlines()]
217
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
 
218
194
class RsyncNoFile(Exception):
219
195
    def __init__(self, path):
220
196
        Exception.__init__(self, "No such file %s" % path)
223
199
    def __init__(self):
224
200
        Exception.__init__(self, "Error in rsync protocol data stream.")
225
201
 
226
 
def get_revision_history(location):
 
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):
227
213
    tempdir = tempfile.mkdtemp('push')
 
214
    my_rsync = _rsync
 
215
    if my_rsync is None:
 
216
        my_rsync = rsync
228
217
    try:
229
218
        history_fname = os.path.join(tempdir, 'revision-history')
230
 
        cmd = rsync(location+'.bzr/revision-history', history_fname,
231
 
                    silent=True)
 
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)
232
225
        history = read_revision_history(history_fname)
233
226
    finally:
234
227
        shutil.rmtree(tempdir)
235
228
    return history
236
229
 
237
 
def history_subset(location, branch):
238
 
    remote_history = get_revision_history(location)
 
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):
239
247
    local_history = branch.revision_history()
240
 
    if len(remote_history) > len(local_history):
241
 
        return False
242
 
    for local, remote in zip(remote_history, local_history):
243
 
        if local != remote:
244
 
            return False 
245
 
    return True
 
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
 
246
263
 
247
264
def empty_or_absent(location):
248
265
    try:
251
268
    except RsyncNoFile:
252
269
        return True
253
270
 
254
 
def push(cur_branch, location=None, overwrite=False):
255
 
    push_location = get_push_data(cur_branch)
256
 
    if location is not None:
257
 
        if not location.endswith('/'):
258
 
            location += '/'
259
 
        push_location = location
260
 
    
261
 
    if push_location is None:
262
 
        print "No push location saved.  Please specify one on the command line."
263
 
        sys.exit(1)
264
 
 
265
 
    clean, non_source = is_clean(cur_branch)
266
 
    if not clean:
267
 
        print """Error: This tree has uncommitted changes or unknown (?) files.
268
 
Use "bzr status" to list them."""
269
 
        sys.exit(1)
270
 
    non_source.extend(exclusions)
271
 
    if not overwrite:
272
 
        try:
273
 
            if not history_subset(push_location, cur_branch):
274
 
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
275
 
                                                    " newer version of remote"
276
 
                                                    " branch.")
277
 
        except RsyncNoFile:
278
 
            if not empty_or_absent(push_location):
279
 
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
280
 
                                                    " bzr branch (or empty"
281
 
                                                    " directory)")
282
 
        except RsyncStreamIO:
283
 
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
284
 
                " specified location.  Please ensure that"
285
 
                ' "%s" is of the form "machine:/path".' % push_location)
286
 
    print "Pushing to %s" % push_location
287
 
    rsync(cur_branch.base+'/', push_location, ssh=True, excludes=non_source)
288
 
 
289
 
    set_push_data(cur_branch, push_location)
 
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
    return get_transport(dirname).get(basename)
 
412
 
290
413
 
291
414
def run_tests():
292
415
    import doctest