~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Robert Collins
  • Date: 2006-05-02 02:36:38 UTC
  • mto: (364.1.3 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 366.
  • Revision ID: robertc@robertcollins.net-20060502023638-9d96d5e0f069cc52
Convert push to rpush.

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")
 
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")
67
72
    push_file.write("%s\n" % location)
68
73
 
69
 
def get_push_data(br):
 
74
def get_push_data(tree):
70
75
    """
71
 
    >>> br = temp_branch()
72
 
    >>> get_push_data(br) is None
 
76
    >>> tree = temp_tree()
 
77
    >>> get_push_data(tree) is None
73
78
    True
74
 
    >>> set_push_data(br, 'http://somewhere')
75
 
    >>> get_push_data(br)
 
79
    >>> set_push_data(tree, 'http://somewhere')
 
80
    >>> get_push_data(tree)
76
81
    'http://somewhere'
77
 
    >>> rm_branch(br)
 
82
    >>> rm_tree(tree)
78
83
    """
79
 
    filename = br.controlfilename("x-push-data")
 
84
    filename = tree.branch.control_files.controlfilename("x-push-data")
80
85
    if not os.path.exists(filename):
81
86
        return None
82
87
    push_file = file (filename, "rb")
101
106
    arg_str = " ".join([shell_escape(a) for a in args])
102
107
    return os.system(arg_str)
103
108
 
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"]
 
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"]
116
136
    if ssh:
117
137
        cmd.extend(('-e', 'ssh'))
118
 
    for exclude in exclude_globs:
119
 
        cmd.extend(('--exclude', exclude))
 
138
    if len(excludes) > 0:
 
139
        cmd.extend(('--exclude-from', '-'))
120
140
    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)
 
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 rpush(tree, location=None, overwrite=False, working_tree=True):
 
245
    push_location = get_push_data(tree)
153
246
    if location is not None:
154
247
        if not location.endswith('/'):
155
248
            location += '/'
156
249
        push_location = location
157
250
    
158
251
    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
 
 
 
252
        raise BzrCommandError("No rpush location known or specified.")
 
253
 
 
254
    if (push_location.find('://') != -1 or
 
255
        push_location.find(':') == -1):
 
256
        raise BzrCommandError("Invalid rsync path %r." % push_location)
 
257
 
 
258
    if working_tree:
 
259
        clean, non_source = is_clean(tree)
 
260
        if not clean:
 
261
            print """Error: This tree has uncommitted changes or unknown (?) files.
 
262
    Use "bzr status" to list them."""
 
263
            sys.exit(1)
 
264
        final_exclusions = non_source[:]
 
265
    else:
 
266
        wt = tree
 
267
        final_exclusions = []
 
268
        for path, status, kind, file_id, entry in wt.list_files():
 
269
            final_exclusions.append(path)
 
270
 
 
271
    final_exclusions.extend(exclusions)
 
272
    if not overwrite:
 
273
        try:
 
274
            if not history_subset(push_location, tree.branch):
 
275
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
276
                                                    " newer version of remote"
 
277
                                                    " branch.")
 
278
        except RsyncNoFile:
 
279
            if not empty_or_absent(push_location):
 
280
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
281
                                                    " bzr branch (or empty"
 
282
                                                    " directory)")
 
283
        except RsyncStreamIO:
 
284
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
285
                " specified location.  Please ensure that"
 
286
                ' "%s" is of the form "machine:/path".' % push_location)
166
287
    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)
 
288
    rsync(tree.basedir+'/', push_location, ssh=True, 
 
289
          excludes=final_exclusions)
 
290
 
 
291
    set_push_data(tree, push_location)
 
292
 
 
293
 
 
294
def short_committer(committer):
 
295
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
296
    if len(new_committer) < 2:
 
297
        return committer
 
298
    return new_committer
 
299
 
 
300
 
 
301
def apache_ls(t):
 
302
    """Screen-scrape Apache listings"""
 
303
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
 
304
        ' <a href="'
 
305
    lines = t.get('.')
 
306
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
 
307
    for line in lines:
 
308
        match = expr.search(line)
 
309
        if match is None:
 
310
            continue
 
311
        url = match.group(1)
 
312
        if url.startswith('http://') or url.startswith('/') or '../' in url:
 
313
            continue
 
314
        if '?' in url:
 
315
            continue
 
316
        yield url.rstrip('/')
 
317
 
 
318
 
 
319
def iter_branches(t, lister=None):
 
320
    """Iterate through all the branches under a transport"""
 
321
    for bzrdir in iter_bzrdirs(t, lister):
 
322
        try:
 
323
            branch = bzrdir.open_branch()
 
324
            if branch.bzrdir is bzrdir:
 
325
                yield branch
 
326
        except (NotBranchError, UnsupportedFormatError):
 
327
            pass
 
328
 
 
329
 
 
330
def iter_branch_tree(t, lister=None):
 
331
    for bzrdir in iter_bzrdirs(t, lister):
 
332
        try:
 
333
            wt = bzrdir.open_workingtree()
 
334
            yield wt.branch, wt
 
335
        except NoWorkingTree, UnsupportedFormatError:
 
336
            try:
 
337
                branch = bzrdir.open_branch()
 
338
                if branch.bzrdir is bzrdir:
 
339
                    yield branch, None
 
340
            except (NotBranchError, UnsupportedFormatError):
 
341
                continue
 
342
 
 
343
 
 
344
def iter_bzrdirs(t, lister=None):
 
345
    if lister is None:
 
346
        def lister(t):
 
347
            return t.list_dir('.')
 
348
    try:
 
349
        bzrdir = bzrdir_from_transport(t)
 
350
        yield bzrdir
 
351
    except (NotBranchError, UnsupportedFormatError, TransportError,
 
352
            PermissionDenied):
 
353
        pass
 
354
    try:
 
355
        for directory in lister(t):
 
356
            if directory == ".bzr":
 
357
                continue
 
358
            try:
 
359
                subt = t.clone(directory)
 
360
            except UnicodeDecodeError:
 
361
                continue
 
362
            for bzrdir in iter_bzrdirs(subt, lister):
 
363
                yield bzrdir
 
364
    except (NoSuchFile, PermissionDenied, TransportError):
 
365
        pass
 
366
 
 
367
    
 
368
def bzrdir_from_transport(t):
 
369
    """Open a bzrdir from a transport (not a location)"""
 
370
    format = BzrDirFormat.find_format(t)
 
371
    BzrDir._check_supported(format, False)
 
372
    return format.open(t)
 
373
 
171
374
 
172
375
def run_tests():
173
376
    import doctest