~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-06-27 14:36:32 UTC
  • Revision ID: abentley@panoramicfeedback.com-20060627143632-0f4114d7b0a8d7d9
Fix zap for checkouts of branches with no parents

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