~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2005-06-08 22:51:36 UTC
  • Revision ID: abentley@bruiser-20050608225136-c900df20f33fb550
Updated copyright

Show diffs side-by-side

added added

removed removed

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