~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2005-10-04 12:31:20 UTC
  • mto: (147.2.17)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: abentley@panoramicfeedback.com-20051004123120-d1d27c1ccf77f51a
Fixed log conversion

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