~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

Make work with integration again.

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
 
18
import bzrlib.errors
19
19
import os
20
 
import re
 
20
import os.path
 
21
import sys
21
22
import tempfile
22
23
import shutil
 
24
import errno
23
25
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():
 
26
import codecs
 
27
 
 
28
def temp_branch():
44
29
    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):
 
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):
51
36
    """
52
37
    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
 
    old_tree = cur_tree.basis_tree()
55
 
    new_tree = cur_tree
 
54
    from bzrlib.diff import compare_trees
 
55
    old_tree = cur_branch.basis_tree()
 
56
    new_tree = cur_branch.working_tree()
56
57
    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()
 
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)
65
62
    return not delta.has_changed(), non_source
66
63
 
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
 
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
74
93
    True
75
 
    >>> set_push_data(tree, 'http://somewhere')
76
 
    >>> get_push_data(tree)
77
 
    u'http://somewhere'
78
 
    >>> rm_tree(tree)
 
94
    >>> set_push_data(br, 'http://somewhere')
 
95
    >>> get_push_data(br)
 
96
    'http://somewhere'
 
97
    >>> rm_branch(br)
79
98
    """
80
 
    try:
81
 
        location = tree.branch._transport.get('x-push-data').read()
82
 
    except NoSuchFile:
 
99
    filename = br.controlfilename("x-push-data")
 
100
    if not os.path.exists(filename):
83
101
        return None
84
 
    location = location.decode('utf-8')
85
 
    return location.rstrip('\n')
 
102
    push_file = file (filename, "rb")
 
103
    (location,) = [f.rstrip('\n') for f in push_file]
 
104
    return location
86
105
 
87
106
"""
88
107
>>> shell_escape('hello')
110
129
    def __init__(self, rsync_name):
111
130
        Exception.__init__(self, "%s not found." % rsync_name)
112
131
 
113
 
 
114
 
def rsync(source, target, ssh=False, excludes=(), silent=False,
 
132
def rsync(source, target, ssh=False, excludes=(), silent=False, 
115
133
          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
    """
116
145
    cmd = [rsync_name, "-av", "--delete"]
117
146
    if ssh:
118
147
        cmd.extend(('-e', 'ssh'))
130
159
    except OSError, e:
131
160
        if e.errno == errno.ENOENT:
132
161
            raise NoRsync(rsync_name)
133
 
 
 
162
            
134
163
    proc.stdin.write('\n'.join(excludes)+'\n')
135
164
    proc.stdin.close()
136
165
    if silent:
172
201
        raise RsyncUnknownStatus(proc.returncode)
173
202
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
174
203
 
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')
 
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')
179
207
 
180
208
 
181
209
def read_revision_history(fname):
182
210
    return [l.rstrip('\r\n') for l in
183
211
            codecs.open(fname, 'rb', 'utf-8').readlines()]
184
212
 
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
213
class RsyncNoFile(Exception):
195
214
    def __init__(self, path):
196
215
        Exception.__init__(self, "No such file %s" % path)
199
218
    def __init__(self):
200
219
        Exception.__init__(self, "Error in rsync protocol data stream.")
201
220
 
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):
 
221
def get_revision_history(location):
213
222
    tempdir = tempfile.mkdtemp('push')
214
 
    my_rsync = _rsync
215
 
    if my_rsync is None:
216
 
        my_rsync = rsync
217
223
    try:
218
224
        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
        cmd = rsync(location+'.bzr/revision-history', history_fname,
 
226
                    silent=True)
225
227
        history = read_revision_history(history_fname)
226
228
    finally:
227
229
        shutil.rmtree(tempdir)
228
230
    return history
229
231
 
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):
 
232
def history_subset(location, branch):
 
233
    remote_history = get_revision_history(location)
247
234
    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
 
 
 
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
263
241
 
264
242
def empty_or_absent(location):
265
243
    try:
268
246
    except RsyncNoFile:
269
247
        return True
270
248
 
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
 
 
 
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)
415
290
 
416
291
def run_tests():
417
292
    import doctest