~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-05-03 19:21:08 UTC
  • mto: This revision was merged to the branch mainline in revision 366.
  • Revision ID: abentley@panoramicfeedback.com-20060503192108-d01a4be5ddc139aa
Patch from robert to allow import into empty branches

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 push(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
        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)
166
292
    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)
 
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
 
171
379
 
172
380
def run_tests():
173
381
    import doctest