~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Max Bowsher
  • Date: 2009-11-13 00:43:08 UTC
  • mto: This revision was merged to the branch mainline in revision 739.
  • Revision ID: maxb@f2s.com-20091113004308-4dcwu1kwt3lhf1kp
Having discovered that bzr-builddeb import_dsc.py is a horrid copy-paste job of bzrtools upstream_import.py, restructure the change to minimize divergence from it.

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
 
17
import codecs
 
18
import errno
18
19
import os
19
 
import os.path
20
 
import sys
 
20
import re
21
21
import tempfile
22
22
import shutil
23
23
from subprocess import Popen, PIPE
24
 
 
25
 
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():
26
44
    dirname = tempfile.mkdtemp("temp-branch")
27
 
    return bzrlib.branch.Branch.initialize(dirname)
28
 
 
29
 
def rm_branch(br):
30
 
    shutil.rmtree(br.base)
31
 
 
32
 
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):
33
51
    """
34
52
    Return true if no files are modifed or unknown
35
 
    >>> import bzrlib.add
36
 
    >>> br = temp_branch()
37
 
    >>> is_clean(br)
38
 
    (True, [])
39
 
    >>> fooname = os.path.join(br.base, "foo")
40
 
    >>> file(fooname, "wb").write("bar")
41
 
    >>> is_clean(br)
42
 
    (True, ['foo'])
43
 
    >>> bzrlib.add.smart_add_branch(br, [br.base])
44
 
    1
45
 
    >>> is_clean(br)
46
 
    (False, [])
47
 
    >>> br.commit("added file")
48
 
    added foo
49
 
    >>> is_clean(br)
50
 
    (True, [])
51
 
    >>> rm_branch(br)
52
53
    """
53
 
    from bzrlib.diff import compare_trees
54
 
    old_tree = cur_branch.basis_tree()
55
 
    new_tree = cur_branch.working_tree()
 
54
    old_tree = cur_tree.basis_tree()
 
55
    new_tree = cur_tree
56
56
    non_source = []
57
 
    for path, file_class, kind, file_id in new_tree.list_files():
58
 
        if file_class in ('?', 'I'):
59
 
            non_source.append(path)
60
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
61
 
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
62
 
        len(delta.modified) > 0:
63
 
        return False, non_source
64
 
    return True, non_source 
65
 
 
66
 
def set_pull_data(br, location, rev_id):
67
 
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
68
 
    pull_file.write("%s\n%s\n" % (location, rev_id))
69
 
 
70
 
def get_pull_data(br):
71
 
    """
72
 
    >>> br = temp_branch()
73
 
    >>> get_pull_data(br)
74
 
    (None, None)
75
 
    >>> set_pull_data(br, 'http://somewhere', '888-777')
76
 
    >>> get_pull_data(br)
77
 
    ('http://somewhere', '888-777')
78
 
    >>> rm_branch(br)
79
 
    """
80
 
    filename = br.controlfilename("x-pull-data")
81
 
    if not os.path.exists(filename):
82
 
        return (None, None)
83
 
    pull_file = file (filename, "rb")
84
 
    location, rev_id = [f.rstrip('\n') for f in pull_file]
85
 
    return location, rev_id
86
 
 
87
 
def set_push_data(br, location):
88
 
    push_file = file (br.controlfilename("x-push-data"), "wb")
89
 
    push_file.write("%s\n" % location)
90
 
 
91
 
def get_push_data(br):
92
 
    """
93
 
    >>> br = temp_branch()
94
 
    >>> 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
95
74
    True
96
 
    >>> set_push_data(br, 'http://somewhere')
97
 
    >>> get_push_data(br)
98
 
    'http://somewhere'
99
 
    >>> rm_branch(br)
 
75
    >>> set_push_data(tree, 'http://somewhere')
 
76
    >>> get_push_data(tree)
 
77
    u'http://somewhere'
 
78
    >>> rm_tree(tree)
100
79
    """
101
 
    filename = br.controlfilename("x-push-data")
102
 
    if not os.path.exists(filename):
 
80
    try:
 
81
        location = tree.branch._transport.get('x-push-data').read()
 
82
    except NoSuchFile:
103
83
        return None
104
 
    push_file = file (filename, "rb")
105
 
    (location,) = [f.rstrip('\n') for f in push_file]
106
 
    return location
 
84
    location = location.decode('utf-8')
 
85
    return location.rstrip('\n')
107
86
 
108
87
"""
109
88
>>> shell_escape('hello')
123
102
    arg_str = " ".join([shell_escape(a) for a in args])
124
103
    return os.system(arg_str)
125
104
 
126
 
def rsync(source, target, ssh=False, excludes=()):
127
 
    """
128
 
    >>> real_system = os.system
129
 
    >>> os.system = sys.stdout.write
130
 
    >>> rsync("a", "b")
131
 
    ['rsync', '-av', '--delete', 'a', 'b']
132
 
    >>> rsync("a", "b", excludes=("*.py",))
133
 
    ['rsync', '-av', '--delete', '--exclude-from', '-', 'a', 'b']
134
 
    >>> os.system = real_system
135
 
    """
136
 
    cmd = ["rsync", "-av", "--delete"]
 
105
class RsyncUnknownStatus(Exception):
 
106
    def __init__(self, status):
 
107
        Exception.__init__(self, "Unknown status: %d" % status)
 
108
 
 
109
class NoRsync(Exception):
 
110
    def __init__(self, rsync_name):
 
111
        Exception.__init__(self, "%s not found." % rsync_name)
 
112
 
 
113
 
 
114
def rsync(source, target, ssh=False, excludes=(), silent=False,
 
115
          rsync_name="rsync"):
 
116
    cmd = [rsync_name, "-av", "--delete"]
137
117
    if ssh:
138
118
        cmd.extend(('-e', 'ssh'))
139
119
    if len(excludes) > 0:
140
120
        cmd.extend(('--exclude-from', '-'))
141
121
    cmd.extend((source, target))
142
 
    proc = Popen(cmd, stdin=PIPE)
 
122
    if silent:
 
123
        stderr = PIPE
 
124
        stdout = PIPE
 
125
    else:
 
126
        stderr = None
 
127
        stdout = None
 
128
    try:
 
129
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
130
    except OSError, e:
 
131
        if e.errno == errno.ENOENT:
 
132
            raise NoRsync(rsync_name)
 
133
 
143
134
    proc.stdin.write('\n'.join(excludes)+'\n')
144
135
    proc.stdin.close()
 
136
    if silent:
 
137
        proc.stderr.read()
 
138
        proc.stderr.close()
 
139
        proc.stdout.read()
 
140
        proc.stdout.close()
145
141
    proc.wait()
 
142
    if proc.returncode == 12:
 
143
        raise RsyncStreamIO()
 
144
    elif proc.returncode == 23:
 
145
        raise RsyncNoFile(source)
 
146
    elif proc.returncode != 0:
 
147
        raise RsyncUnknownStatus(proc.returncode)
146
148
    return cmd
147
149
 
148
 
exclusions = ('.bzr/x-push-data', '.bzr/x-pull-data', '.bzr/stat-cache')
149
 
 
150
 
 
151
 
def push(cur_branch, location=None):
152
 
    push_location = get_push_data(cur_branch)
153
 
    if location is not None:
154
 
        if not location.endswith('/'):
155
 
            location += '/'
156
 
        push_location = location
157
 
    
158
 
    if push_location is None:
159
 
        print "No push location saved.  Please specify one on the command line."
160
 
        sys.exit(1)
161
 
 
162
 
    clean, non_source = is_clean(cur_branch)
163
 
    if not clean:
164
 
        print """Error: This tree has uncommitted changes or unknown (?) files.
165
 
Use "bzr status" to list them."""
166
 
        sys.exit(1)
167
 
    non_source.extend(exclusions)
168
 
 
169
 
    print "Pushing to %s" % push_location
170
 
    rsync(cur_branch.base+'/', push_location, ssh=True, excludes=non_source)
171
 
 
172
 
    set_push_data(cur_branch, push_location)
 
150
 
 
151
def rsync_ls(source, ssh=False, silent=True):
 
152
    cmd = ["rsync"]
 
153
    if ssh:
 
154
        cmd.extend(('-e', 'ssh'))
 
155
    cmd.append(source)
 
156
    if silent:
 
157
        stderr = PIPE
 
158
    else:
 
159
        stderr = None
 
160
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
161
    result = proc.stdout.read()
 
162
    proc.stdout.close()
 
163
    if silent:
 
164
        proc.stderr.read()
 
165
        proc.stderr.close()
 
166
    proc.wait()
 
167
    if proc.returncode == 12:
 
168
        raise RsyncStreamIO()
 
169
    elif proc.returncode == 23:
 
170
        raise RsyncNoFile(source)
 
171
    elif proc.returncode != 0:
 
172
        raise RsyncUnknownStatus(proc.returncode)
 
173
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
174
 
 
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')
 
179
 
 
180
 
 
181
def read_revision_history(fname):
 
182
    return [l.rstrip('\r\n') for l in
 
183
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
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
 
 
194
class RsyncNoFile(Exception):
 
195
    def __init__(self, path):
 
196
        Exception.__init__(self, "No such file %s" % path)
 
197
 
 
198
class RsyncStreamIO(Exception):
 
199
    def __init__(self):
 
200
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
201
 
 
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):
 
213
    tempdir = tempfile.mkdtemp('push')
 
214
    my_rsync = _rsync
 
215
    if my_rsync is None:
 
216
        my_rsync = rsync
 
217
    try:
 
218
        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
        history = read_revision_history(history_fname)
 
226
    finally:
 
227
        shutil.rmtree(tempdir)
 
228
    return history
 
229
 
 
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):
 
247
    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
 
 
263
 
 
264
def empty_or_absent(location):
 
265
    try:
 
266
        files = rsync_ls(location)
 
267
        return files == ['.']
 
268
    except RsyncNoFile:
 
269
        return True
 
270
 
 
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
 
173
415
 
174
416
def run_tests():
175
417
    import doctest