~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-12-12 16:50:31 UTC
  • Revision ID: abentley@panoramicfeedback.com-20061212165031-51w8gjy1eps1vnw0
update NEWS

Show diffs side-by-side

added added

removed removed

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