~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-03-24 17:24:21 UTC
  • Revision ID: abentley@panoramicfeedback.com-20060324172421-c1acc18c1a4075a6
Added multi-pull, working on branches and checkouts

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
#    You should have received a copy of the GNU General Public License
15
15
#    along with this program; if not, write to the Free Software
16
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
import bzrlib
 
17
import codecs
 
18
import errno
18
19
import os
19
 
import os.path
20
 
import sys
 
20
import re
21
21
import tempfile
22
22
import shutil
23
 
 
24
 
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)
 
31
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
32
 
 
33
def temp_tree():
25
34
    dirname = tempfile.mkdtemp("temp-branch")
26
 
    return bzrlib.Branch(dirname, init=True)
27
 
 
28
 
def rm_branch(br):
29
 
    shutil.rmtree(br.base)
30
 
 
31
 
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):
32
41
    """
33
42
    Return true if no files are modifed or unknown
34
 
    >>> br = temp_branch()
35
 
    >>> is_clean(br)
36
 
    True
37
 
    >>> 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")
38
48
    >>> file(fooname, "wb").write("bar")
39
 
    >>> is_clean(br)
40
 
    False
41
 
    >>> bzrlib.add.smart_add([fooname])
42
 
    >>> is_clean(br)
43
 
    False
44
 
    >>> br.commit("added file")
45
 
    >>> is_clean(br)
46
 
    True
47
 
    >>> rm_branch(br)
48
 
    """
49
 
    old_tree = cur_branch.basis_tree()
50
 
    new_tree = cur_branch.working_tree()
51
 
    for path, file_class, kind, file_id in new_tree.list_files():
52
 
        if file_class == '?':
53
 
            return False
54
 
    delta = bzrlib.compare_trees(old_tree, new_tree, want_unchanged=False)
55
 
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
56
 
        len(delta.modified) > 0:
57
 
        return False
58
 
    return True
59
 
 
60
 
def set_pull_data(br, location, rev_id):
61
 
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
62
 
    pull_file.write("%s\n%s\n" % (location, rev_id))
63
 
 
64
 
def get_pull_data(br):
65
 
    """
66
 
    >>> br = temp_branch()
67
 
    >>> get_pull_data(br)
68
 
    (None, None)
69
 
    >>> set_pull_data(br, 'http://somewhere', '888-777')
70
 
    >>> get_pull_data(br)
71
 
    ('http://somewhere', '888-777')
72
 
    >>> rm_branch(br)
73
 
    """
74
 
    filename = br.controlfilename("x-pull-data")
75
 
    if not os.path.exists(filename):
76
 
        return (None, None)
77
 
    pull_file = file (filename, "rb")
78
 
    location, rev_id = [f.rstrip('\n') for f in pull_file]
79
 
    return location, rev_id
80
 
 
81
 
def set_push_data(br, location):
82
 
    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")
83
72
    push_file.write("%s\n" % location)
84
73
 
85
 
def get_push_data(br):
 
74
def get_push_data(tree):
86
75
    """
87
 
    >>> br = temp_branch()
88
 
    >>> get_push_data(br) is None
 
76
    >>> tree = temp_tree()
 
77
    >>> get_push_data(tree) is None
89
78
    True
90
 
    >>> set_push_data(br, 'http://somewhere')
91
 
    >>> get_push_data(br)
 
79
    >>> set_push_data(tree, 'http://somewhere')
 
80
    >>> get_push_data(tree)
92
81
    'http://somewhere'
93
 
    >>> rm_branch(br)
 
82
    >>> rm_tree(tree)
94
83
    """
95
 
    filename = br.controlfilename("x-push-data")
 
84
    filename = tree.branch.control_files.controlfilename("x-push-data")
96
85
    if not os.path.exists(filename):
97
86
        return None
98
87
    push_file = file (filename, "rb")
117
106
    arg_str = " ".join([shell_escape(a) for a in args])
118
107
    return os.system(arg_str)
119
108
 
120
 
def rsync(source, target, ssh=False, exclude_globs=()):
121
 
    """
122
 
    >>> real_system = os.system
123
 
    >>> os.system = sys.stdout.write
124
 
    >>> rsync("a", "b")
125
 
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e \\a \\b
126
 
    >>> rsync("a", "b", exclude_globs=("*.py",))
127
 
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e\
128
 
 \\-\\-\\e\\x\\c\\l\\u\\d\\e \\*\\.\\p\\y \\a \\b
129
 
    >>> os.system = real_system
130
 
    """
131
 
    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"]
132
136
    if ssh:
133
137
        cmd.extend(('-e', 'ssh'))
134
 
    for exclude in exclude_globs:
135
 
        cmd.extend(('--exclude', exclude))
 
138
    if len(excludes) > 0:
 
139
        cmd.extend(('--exclude-from', '-'))
136
140
    cmd.extend((source, target))
137
 
    safe_system(cmd)
138
 
 
139
 
exclusions = ('x-push-data', 'x-pull-data')
140
 
 
141
 
 
142
 
def pull(cur_branch, location=None, overwrite=False):
143
 
    pull_location, pull_revision = get_pull_data(cur_branch)
144
 
    if pull_location is not None:
145
 
        if not overwrite and cur_branch.last_patch() != pull_revision:
146
 
            print "Aborting: This branch has had commits, so pull would lose data."
147
 
            sys.exit(1)
148
 
    if location is not None:
149
 
        pull_location = location
150
 
        if not pull_location.endswith('/'):
151
 
            pull_location+='/'
152
 
 
153
 
    if pull_location is None:
154
 
        print "No pull location saved.  Please specify one on the command line."
155
 
        sys.exit(1)
156
 
 
157
 
    if not is_clean(cur_branch):
158
 
        print "Error: This tree has uncommitted changes or unknown (?) files."
159
 
        sys.exit(1)
160
 
 
161
 
    print "Synchronizing with %s" % pull_location
162
 
    rsync (pull_location, cur_branch.base+'/', exclude_globs=exclusions)
163
 
 
164
 
    set_pull_data(cur_branch, pull_location, cur_branch.last_patch())
165
 
 
166
 
 
167
 
def push(cur_branch, location=None):
168
 
    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)
169
246
    if location is not None:
170
247
        if not location.endswith('/'):
171
248
            location += '/'
172
249
        push_location = location
173
250
    
174
251
    if push_location is None:
175
 
        print "No push location saved.  Please specify one on the command line."
176
 
        sys.exit(1)
177
 
 
178
 
    if not is_clean(cur_branch):
179
 
        print """Error: This tree has uncommitted changes or unknown (?) files.
180
 
Use "bzr status" to list them."""
181
 
        sys.exit(1)
182
 
 
 
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)
183
292
    print "Pushing to %s" % push_location
184
 
    rsync(cur_branch.base+'/', push_location, ssh=True,
185
 
          exclude_globs=exclusions)
186
 
 
187
 
    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 iter_branches(t):
 
307
    """Iterate through all the branches under a transport"""
 
308
    for bzrdir in iter_bzrdirs(t):
 
309
        try:
 
310
            branch = bzrdir.open_branch()
 
311
            if branch.bzrdir is bzrdir:
 
312
                yield branch
 
313
        except (NotBranchError, UnsupportedFormatError):
 
314
            pass
 
315
 
 
316
def iter_branch_tree(t):
 
317
    for bzrdir in iter_bzrdirs(t):
 
318
        try:
 
319
            wt = bzrdir.open_workingtree()
 
320
            yield wt.branch, wt
 
321
        except NoWorkingTree, UnsupportedFormatError:
 
322
            try:
 
323
                branch = bzrdir.open_branch()
 
324
                if branch.bzrdir is bzrdir:
 
325
                    yield branch, None
 
326
            except (NotBranchError, UnsupportedFormatError):
 
327
                continue
 
328
 
 
329
def iter_bzrdirs(t):
 
330
    try:
 
331
        bzrdir = bzrdir_from_transport(t)
 
332
        yield bzrdir
 
333
    except (NotBranchError, UnsupportedFormatError, TransportError):
 
334
        pass
 
335
    try:
 
336
        for directory in t.list_dir('.'):
 
337
            if directory == ".bzr":
 
338
                continue
 
339
            subt = t.clone(directory)
 
340
            for bzrdir in iter_bzrdirs(subt):
 
341
                yield bzrdir
 
342
    except NoSuchFile:
 
343
        pass
 
344
    except TransportError, e:
 
345
        pass
 
346
 
 
347
    
 
348
def bzrdir_from_transport(t):
 
349
    """Open a bzrdir from a transport (not a location)"""
 
350
    format = BzrDirFormat.find_format(t)
 
351
    BzrDir._check_supported(format, False)
 
352
    return format.open(t)
 
353
 
188
354
 
189
355
def run_tests():
190
356
    import doctest