~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Jelmer Vernooij
  • Date: 2011-03-13 16:06:41 UTC
  • mto: This revision was merged to the branch mainline in revision 769.
  • Revision ID: jelmer@samba.org-20110313160641-tcsdktixq0588tz6
Merge in bzrlib.deprecated_graph functions still used by bzrtools.

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, [u'foo'])
46
 
    >>> br.working_tree().add(["foo"])
47
 
    >>> is_clean(br)
48
 
    (False, [])
49
 
    >>> br.working_tree().commit("added file")
50
 
    >>> is_clean(br)
51
 
    (True, [])
52
 
    >>> rm_branch(br)
53
53
    """
54
 
    from bzrlib.diff import compare_trees
55
 
    old_tree = cur_branch.basis_tree()
56
 
    new_tree = cur_branch.working_tree()
 
54
    old_tree = cur_tree.basis_tree()
 
55
    new_tree = cur_tree
57
56
    non_source = []
58
 
    for path, file_class, kind, file_id, entry in new_tree.list_files():
59
 
        if file_class in ('?', 'I'):
60
 
            non_source.append(path)
61
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
 
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()
62
65
    return not delta.has_changed(), non_source
63
66
 
64
 
def set_pull_data(br, location, rev_id):
65
 
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
66
 
    pull_file.write("%s\n%s\n" % (location, rev_id))
67
 
 
68
 
def get_pull_data(br):
69
 
    """
70
 
    >>> br = temp_branch()
71
 
    >>> get_pull_data(br)
72
 
    (None, None)
73
 
    >>> set_pull_data(br, 'http://somewhere', '888-777')
74
 
    >>> get_pull_data(br)
75
 
    ('http://somewhere', '888-777')
76
 
    >>> rm_branch(br)
77
 
    """
78
 
    filename = br.controlfilename("x-pull-data")
79
 
    if not os.path.exists(filename):
80
 
        return (None, None)
81
 
    pull_file = file (filename, "rb")
82
 
    location, rev_id = [f.rstrip('\n') for f in pull_file]
83
 
    return location, rev_id
84
 
 
85
 
def set_push_data(br, location):
86
 
    push_file = file (br.controlfilename("x-push-data"), "wb")
87
 
    push_file.write("%s\n" % location)
88
 
 
89
 
def get_push_data(br):
90
 
    """
91
 
    >>> br = temp_branch()
92
 
    >>> get_push_data(br) is None
 
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
93
74
    True
94
 
    >>> set_push_data(br, 'http://somewhere')
95
 
    >>> get_push_data(br)
96
 
    'http://somewhere'
97
 
    >>> rm_branch(br)
 
75
    >>> set_push_data(tree, 'http://somewhere')
 
76
    >>> get_push_data(tree)
 
77
    u'http://somewhere'
 
78
    >>> rm_tree(tree)
98
79
    """
99
 
    filename = br.controlfilename("x-push-data")
100
 
    if not os.path.exists(filename):
 
80
    try:
 
81
        location = tree.branch._transport.get('x-push-data').read()
 
82
    except NoSuchFile:
101
83
        return None
102
 
    push_file = file (filename, "rb")
103
 
    (location,) = [f.rstrip('\n') for f in push_file]
104
 
    return location
 
84
    location = location.decode('utf-8')
 
85
    return location.rstrip('\n')
105
86
 
106
87
"""
107
88
>>> shell_escape('hello')
129
110
    def __init__(self, rsync_name):
130
111
        Exception.__init__(self, "%s not found." % rsync_name)
131
112
 
132
 
def rsync(source, target, ssh=False, excludes=(), silent=False, 
 
113
 
 
114
def rsync(source, target, ssh=False, excludes=(), silent=False,
133
115
          rsync_name="rsync"):
134
 
    """
135
 
    >>> rsync("a", "b", silent=True)
136
 
    Traceback (most recent call last):
137
 
    RsyncNoFile: No such file a
138
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True)
139
 
    Traceback (most recent call last):
140
 
    RsyncNoFile: No such file a
141
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
142
 
    Traceback (most recent call last):
143
 
    NoRsync: rsyncc not found.
144
 
    """
145
116
    cmd = [rsync_name, "-av", "--delete"]
146
117
    if ssh:
147
118
        cmd.extend(('-e', 'ssh'))
159
130
    except OSError, e:
160
131
        if e.errno == errno.ENOENT:
161
132
            raise NoRsync(rsync_name)
162
 
            
 
133
 
163
134
    proc.stdin.write('\n'.join(excludes)+'\n')
164
135
    proc.stdin.close()
165
136
    if silent:
201
172
        raise RsyncUnknownStatus(proc.returncode)
202
173
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
203
174
 
204
 
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
205
 
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
206
 
              '.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')
207
179
 
208
180
 
209
181
def read_revision_history(fname):
210
182
    return [l.rstrip('\r\n') for l in
211
183
            codecs.open(fname, 'rb', 'utf-8').readlines()]
212
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
 
213
194
class RsyncNoFile(Exception):
214
195
    def __init__(self, path):
215
196
        Exception.__init__(self, "No such file %s" % path)
218
199
    def __init__(self):
219
200
        Exception.__init__(self, "Error in rsync protocol data stream.")
220
201
 
221
 
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):
222
213
    tempdir = tempfile.mkdtemp('push')
 
214
    my_rsync = _rsync
 
215
    if my_rsync is None:
 
216
        my_rsync = rsync
223
217
    try:
224
218
        history_fname = os.path.join(tempdir, 'revision-history')
225
 
        cmd = rsync(location+'.bzr/revision-history', history_fname,
226
 
                    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)
227
225
        history = read_revision_history(history_fname)
228
226
    finally:
229
227
        shutil.rmtree(tempdir)
230
228
    return history
231
229
 
232
 
def history_subset(location, branch):
233
 
    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):
234
247
    local_history = branch.revision_history()
235
 
    if len(remote_history) > len(local_history):
236
 
        return False
237
 
    for local, remote in zip(remote_history, local_history):
238
 
        if local != remote:
239
 
            return False 
240
 
    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
 
241
263
 
242
264
def empty_or_absent(location):
243
265
    try:
246
268
    except RsyncNoFile:
247
269
        return True
248
270
 
249
 
def push(cur_branch, location=None, overwrite=False):
250
 
    push_location = get_push_data(cur_branch)
251
 
    if location is not None:
252
 
        if not location.endswith('/'):
253
 
            location += '/'
254
 
        push_location = location
255
 
    
256
 
    if push_location is None:
257
 
        raise bzrlib.errors.MustUseDecorated
258
 
 
259
 
    if push_location.find('://') != -1:
260
 
        raise bzrlib.errors.MustUseDecorated
261
 
 
262
 
    if push_location.find(':') == -1:
263
 
        raise bzrlib.errors.MustUseDecorated
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
    if location.endswith('/') and not basename.endswith('/'):
 
412
        basename += '/'
 
413
    return get_transport(dirname).get(basename)
 
414
 
290
415
 
291
416
def run_tests():
292
417
    import doctest