~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2007-02-15 20:02:58 UTC
  • Revision ID: abentley@panoramicfeedback.com-20070215200258-w08yzl5tz9wiuxtu
Fix imports

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.bentley@utoronto.ca>
 
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
import bzrlib.errors
 
28
from bzrlib.errors import (BzrCommandError, NotBranchError, NoSuchFile,
 
29
                           UnsupportedFormatError, TransportError, 
 
30
                           NoWorkingTree, PermissionDenied)
 
31
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
32
 
 
33
def temp_tree():
26
34
    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):
 
35
    return BzrDir.create_standalone_workingtree(dirname)
 
36
 
 
37
def rm_tree(tree):
 
38
    shutil.rmtree(tree.basedir)
 
39
 
 
40
def is_clean(cur_tree):
33
41
    """
34
42
    Return true if no files are modifed or unknown
35
43
    >>> import bzrlib.add
36
 
    >>> br = temp_branch()
37
 
    >>> is_clean(br)
 
44
    >>> tree = temp_tree()
 
45
    >>> is_clean(tree)
38
46
    (True, [])
39
 
    >>> fooname = os.path.join(br.base, "foo")
 
47
    >>> fooname = os.path.join(tree.basedir, "foo")
40
48
    >>> 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)
 
49
    >>> is_clean(tree)
 
50
    (True, [u'foo'])
 
51
    >>> bzrlib.add.smart_add_tree(tree, [tree.basedir])
 
52
    ([u'foo'], {})
 
53
    >>> is_clean(tree)
46
54
    (False, [])
47
 
    >>> br.commit("added file")
48
 
    added foo
49
 
    >>> is_clean(br)
 
55
    >>> tree.commit("added file", rev_id='commit-id')
 
56
    'commit-id'
 
57
    >>> is_clean(tree)
50
58
    (True, [])
51
 
    >>> rm_branch(br)
 
59
    >>> rm_tree(tree)
52
60
    """
53
 
    from bzrlib.diff import compare_trees
54
 
    old_tree = cur_branch.basis_tree()
55
 
    new_tree = cur_branch.working_tree()
 
61
    old_tree = cur_tree.basis_tree()
 
62
    new_tree = cur_tree
56
63
    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
 
64
    new_tree.lock_read()
 
65
    try:
 
66
        for path, file_class, kind, file_id, entry in new_tree.list_files():
 
67
            if file_class in ('?', 'I'):
 
68
                non_source.append(path)
 
69
        delta = new_tree.changes_from(old_tree, want_unchanged=False)
 
70
    finally:
 
71
        new_tree.unlock()
 
72
    return not delta.has_changed(), non_source
 
73
 
 
74
def set_push_data(tree, location):
 
75
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
 
76
 
 
77
def get_push_data(tree):
 
78
    """
 
79
    >>> tree = temp_tree()
 
80
    >>> get_push_data(tree) is None
95
81
    True
96
 
    >>> set_push_data(br, 'http://somewhere')
97
 
    >>> get_push_data(br)
98
 
    'http://somewhere'
99
 
    >>> rm_branch(br)
 
82
    >>> set_push_data(tree, 'http://somewhere')
 
83
    >>> get_push_data(tree)
 
84
    u'http://somewhere'
 
85
    >>> rm_tree(tree)
100
86
    """
101
 
    filename = br.controlfilename("x-push-data")
102
 
    if not os.path.exists(filename):
 
87
    try:
 
88
        location = tree.branch.control_files.get_utf8('x-push-data').read()
 
89
    except NoSuchFile:
103
90
        return None
104
 
    push_file = file (filename, "rb")
105
 
    (location,) = [f.rstrip('\n') for f in push_file]
106
 
    return location
 
91
    return location.rstrip('\n')
107
92
 
108
93
"""
109
94
>>> shell_escape('hello')
123
108
    arg_str = " ".join([shell_escape(a) for a in args])
124
109
    return os.system(arg_str)
125
110
 
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"]
 
111
class RsyncUnknownStatus(Exception):
 
112
    def __init__(self, status):
 
113
        Exception.__init__(self, "Unknown status: %d" % status)
 
114
 
 
115
class NoRsync(Exception):
 
116
    def __init__(self, rsync_name):
 
117
        Exception.__init__(self, "%s not found." % rsync_name)
 
118
 
 
119
def rsync(source, target, ssh=False, excludes=(), silent=False, 
 
120
          rsync_name="rsync"):
 
121
    """
 
122
    >>> new_dir = tempfile.mkdtemp()
 
123
    >>> old_dir = os.getcwd()
 
124
    >>> os.chdir(new_dir)
 
125
    >>> rsync("a", "b", silent=True)
 
126
    Traceback (most recent call last):
 
127
    RsyncNoFile: No such file...
 
128
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
 
129
    Traceback (most recent call last):
 
130
    RsyncNoFile: No such file...
 
131
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
 
132
    Traceback (most recent call last):
 
133
    NoRsync: rsyncc not found.
 
134
    >>> os.chdir(old_dir)
 
135
    >>> os.rmdir(new_dir)
 
136
    """
 
137
    cmd = [rsync_name, "-av", "--delete"]
137
138
    if ssh:
138
139
        cmd.extend(('-e', 'ssh'))
139
140
    if len(excludes) > 0:
140
141
        cmd.extend(('--exclude-from', '-'))
141
142
    cmd.extend((source, target))
142
 
    proc = Popen(cmd, stdin=PIPE)
 
143
    if silent:
 
144
        stderr = PIPE
 
145
        stdout = PIPE
 
146
    else:
 
147
        stderr = None
 
148
        stdout = None
 
149
    try:
 
150
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
151
    except OSError, e:
 
152
        if e.errno == errno.ENOENT:
 
153
            raise NoRsync(rsync_name)
 
154
            
143
155
    proc.stdin.write('\n'.join(excludes)+'\n')
144
156
    proc.stdin.close()
 
157
    if silent:
 
158
        proc.stderr.read()
 
159
        proc.stderr.close()
 
160
        proc.stdout.read()
 
161
        proc.stdout.close()
145
162
    proc.wait()
 
163
    if proc.returncode == 12:
 
164
        raise RsyncStreamIO()
 
165
    elif proc.returncode == 23:
 
166
        raise RsyncNoFile(source)
 
167
    elif proc.returncode != 0:
 
168
        raise RsyncUnknownStatus(proc.returncode)
146
169
    return cmd
147
170
 
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)
 
171
 
 
172
def rsync_ls(source, ssh=False, silent=True):
 
173
    cmd = ["rsync"]
 
174
    if ssh:
 
175
        cmd.extend(('-e', 'ssh'))
 
176
    cmd.append(source)
 
177
    if silent:
 
178
        stderr = PIPE
 
179
    else:
 
180
        stderr = None
 
181
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
182
    result = proc.stdout.read()
 
183
    proc.stdout.close()
 
184
    if silent:
 
185
        proc.stderr.read()
 
186
        proc.stderr.close()
 
187
    proc.wait()
 
188
    if proc.returncode == 12:
 
189
        raise RsyncStreamIO()
 
190
    elif proc.returncode == 23:
 
191
        raise RsyncNoFile(source)
 
192
    elif proc.returncode != 0:
 
193
        raise RsyncUnknownStatus(proc.returncode)
 
194
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
195
 
 
196
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent', 
 
197
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
 
198
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
 
199
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
 
200
 
 
201
 
 
202
def read_revision_history(fname):
 
203
    return [l.rstrip('\r\n') for l in
 
204
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
205
 
 
206
class RsyncNoFile(Exception):
 
207
    def __init__(self, path):
 
208
        Exception.__init__(self, "No such file %s" % path)
 
209
 
 
210
class RsyncStreamIO(Exception):
 
211
    def __init__(self):
 
212
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
213
 
 
214
def get_revision_history(location):
 
215
    tempdir = tempfile.mkdtemp('push')
 
216
    try:
 
217
        history_fname = os.path.join(tempdir, 'revision-history')
 
218
        try:
 
219
            cmd = rsync(location+'.bzr/revision-history', history_fname,
 
220
                        silent=True)
 
221
        except RsyncNoFile:
 
222
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
 
223
                        silent=True)
 
224
        history = read_revision_history(history_fname)
 
225
    finally:
 
226
        shutil.rmtree(tempdir)
 
227
    return history
 
228
 
 
229
def history_subset(location, branch):
 
230
    remote_history = get_revision_history(location)
 
231
    local_history = branch.revision_history()
 
232
    if len(remote_history) > len(local_history):
 
233
        return False
 
234
    for local, remote in zip(remote_history, local_history):
 
235
        if local != remote:
 
236
            return False 
 
237
    return True
 
238
 
 
239
def empty_or_absent(location):
 
240
    try:
 
241
        files = rsync_ls(location)
 
242
        return files == ['.']
 
243
    except RsyncNoFile:
 
244
        return True
 
245
 
 
246
def rspush(tree, location=None, overwrite=False, working_tree=True):
 
247
    push_location = get_push_data(tree)
153
248
    if location is not None:
154
249
        if not location.endswith('/'):
155
250
            location += '/'
156
251
        push_location = location
157
252
    
158
253
    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
 
 
 
254
        raise BzrCommandError("No rspush location known or specified.")
 
255
 
 
256
    if (push_location.find('::') != -1):
 
257
        usessh=False
 
258
    else:
 
259
        usessh=True
 
260
 
 
261
    if (push_location.find('://') != -1 or
 
262
        push_location.find(':') == -1):
 
263
        raise BzrCommandError("Invalid rsync path %r." % push_location)
 
264
 
 
265
    if working_tree:
 
266
        clean, non_source = is_clean(tree)
 
267
        if not clean:
 
268
            print """Error: This tree has uncommitted changes or unknown (?) files.
 
269
    Use "bzr status" to list them."""
 
270
            sys.exit(1)
 
271
        final_exclusions = non_source[:]
 
272
    else:
 
273
        wt = tree
 
274
        final_exclusions = []
 
275
        for path, status, kind, file_id, entry in wt.list_files():
 
276
            final_exclusions.append(path)
 
277
 
 
278
    final_exclusions.extend(exclusions)
 
279
    if not overwrite:
 
280
        try:
 
281
            if not history_subset(push_location, tree.branch):
 
282
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
283
                                                    " newer version of remote"
 
284
                                                    " branch.")
 
285
        except RsyncNoFile:
 
286
            if not empty_or_absent(push_location):
 
287
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
288
                                                    " bzr branch (or empty"
 
289
                                                    " directory)")
 
290
        except RsyncStreamIO:
 
291
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
292
                " specified location.  Please ensure that"
 
293
                ' "%s" is of the form "machine:/path".' % push_location)
169
294
    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)
 
295
    rsync(tree.basedir+'/', push_location, ssh=usessh, 
 
296
          excludes=final_exclusions)
 
297
 
 
298
    set_push_data(tree, push_location)
 
299
 
 
300
 
 
301
def short_committer(committer):
 
302
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
303
    if len(new_committer) < 2:
 
304
        return committer
 
305
    return new_committer
 
306
 
 
307
 
 
308
def apache_ls(t):
 
309
    """Screen-scrape Apache listings"""
 
310
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
 
311
        ' <a href="'
 
312
    lines = t.get('.')
 
313
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
 
314
    for line in lines:
 
315
        match = expr.search(line)
 
316
        if match is None:
 
317
            continue
 
318
        url = match.group(1)
 
319
        if url.startswith('http://') or url.startswith('/') or '../' in url:
 
320
            continue
 
321
        if '?' in url:
 
322
            continue
 
323
        yield url.rstrip('/')
 
324
 
 
325
 
 
326
def iter_branches(t, lister=None):
 
327
    """Iterate through all the branches under a transport"""
 
328
    for bzrdir in iter_bzrdirs(t, lister):
 
329
        try:
 
330
            branch = bzrdir.open_branch()
 
331
            if branch.bzrdir is bzrdir:
 
332
                yield branch
 
333
        except (NotBranchError, UnsupportedFormatError):
 
334
            pass
 
335
 
 
336
 
 
337
def iter_branch_tree(t, lister=None):
 
338
    for bzrdir in iter_bzrdirs(t, lister):
 
339
        try:
 
340
            wt = bzrdir.open_workingtree()
 
341
            yield wt.branch, wt
 
342
        except NoWorkingTree, UnsupportedFormatError:
 
343
            try:
 
344
                branch = bzrdir.open_branch()
 
345
                if branch.bzrdir is bzrdir:
 
346
                    yield branch, None
 
347
            except (NotBranchError, UnsupportedFormatError):
 
348
                continue
 
349
 
 
350
 
 
351
def iter_bzrdirs(t, lister=None):
 
352
    if lister is None:
 
353
        def lister(t):
 
354
            return t.list_dir('.')
 
355
    try:
 
356
        bzrdir = bzrdir_from_transport(t)
 
357
        yield bzrdir
 
358
    except (NotBranchError, UnsupportedFormatError, TransportError,
 
359
            PermissionDenied):
 
360
        pass
 
361
    try:
 
362
        for directory in lister(t):
 
363
            if directory == ".bzr":
 
364
                continue
 
365
            try:
 
366
                subt = t.clone(directory)
 
367
            except UnicodeDecodeError:
 
368
                continue
 
369
            for bzrdir in iter_bzrdirs(subt, lister):
 
370
                yield bzrdir
 
371
    except (NoSuchFile, PermissionDenied, TransportError):
 
372
        pass
 
373
 
 
374
    
 
375
def bzrdir_from_transport(t):
 
376
    """Open a bzrdir from a transport (not a location)"""
 
377
    format = BzrDirFormat.find_format(t)
 
378
    BzrDir._check_supported(format, False)
 
379
    return format.open(t)
 
380
 
173
381
 
174
382
def run_tests():
175
383
    import doctest