~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-02-21 04:04:27 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20060221040427-70ecc1e24f2c4e80
forced ancestry graph to use utf-8 Dot output

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
1
17
import bzrlib
 
18
import bzrlib.errors
2
19
import os
3
20
import os.path
4
21
import sys
5
22
import tempfile
6
23
import shutil
 
24
import errno
 
25
from subprocess import Popen, PIPE
 
26
import codecs
 
27
import re
7
28
 
8
29
def temp_branch():
9
30
    dirname = tempfile.mkdtemp("temp-branch")
10
 
    return bzrlib.Branch(dirname, init=True)
 
31
    return bzrlib.branch.Branch.initialize(dirname)
11
32
 
12
33
def rm_branch(br):
13
34
    shutil.rmtree(br.base)
15
36
def is_clean(cur_branch):
16
37
    """
17
38
    Return true if no files are modifed or unknown
 
39
    >>> import bzrlib.add
18
40
    >>> br = temp_branch()
19
41
    >>> is_clean(br)
20
 
    True
 
42
    (True, [])
21
43
    >>> fooname = os.path.join(br.base, "foo")
22
44
    >>> file(fooname, "wb").write("bar")
23
45
    >>> 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
 
46
    (True, [u'foo'])
 
47
    >>> bzrlib.add.smart_add_tree(br.working_tree(), [br.base])
 
48
    ([u'foo'], {})
 
49
    >>> is_clean(br)
 
50
    (False, [])
 
51
    >>> br.working_tree().commit("added file")
 
52
    >>> is_clean(br)
 
53
    (True, [])
31
54
    >>> rm_branch(br)
32
55
    """
 
56
    from bzrlib.diff import compare_trees
33
57
    old_tree = cur_branch.basis_tree()
34
58
    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
 
59
    non_source = []
 
60
    for path, file_class, kind, file_id, entry in new_tree.list_files():
 
61
        if file_class in ('?', 'I'):
 
62
            non_source.append(path)
 
63
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
 
64
    return not delta.has_changed(), non_source
64
65
 
65
66
def set_push_data(br, location):
66
 
    push_file = file (br.controlfilename("x-push-data"), "wb")
 
67
    push_file = file (br.control_files.controlfilename("x-push-data"), "wb")
67
68
    push_file.write("%s\n" % location)
68
69
 
69
70
def get_push_data(br):
76
77
    'http://somewhere'
77
78
    >>> rm_branch(br)
78
79
    """
79
 
    filename = br.controlfilename("x-push-data")
 
80
    filename = br.control_files.controlfilename("x-push-data")
80
81
    if not os.path.exists(filename):
81
82
        return None
82
83
    push_file = file (filename, "rb")
101
102
    arg_str = " ".join([shell_escape(a) for a in args])
102
103
    return os.system(arg_str)
103
104
 
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"]
 
105
class RsyncUnknownStatus(Exception):
 
106
    def __init__(self, status):
 
107
        Exception.__init__(self, "Unknown status: %d" % status)
 
108
 
 
109
class NoRsync(Exception):
 
110
    def __init__(self, rsync_name):
 
111
        Exception.__init__(self, "%s not found." % rsync_name)
 
112
 
 
113
def rsync(source, target, ssh=False, excludes=(), silent=False, 
 
114
          rsync_name="rsync"):
 
115
    """
 
116
    >>> new_dir = tempfile.mkdtemp()
 
117
    >>> old_dir = os.getcwd()
 
118
    >>> os.chdir(new_dir)
 
119
    >>> rsync("a", "b", silent=True)
 
120
    Traceback (most recent call last):
 
121
    RsyncNoFile: No such file a
 
122
    >>> rsync("a", "b", excludes=("*.py",), silent=True)
 
123
    Traceback (most recent call last):
 
124
    RsyncNoFile: No such file a
 
125
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
 
126
    Traceback (most recent call last):
 
127
    NoRsync: rsyncc not found.
 
128
    >>> os.chdir(old_dir)
 
129
    >>> os.rmdir(new_dir)
 
130
    """
 
131
    cmd = [rsync_name, "-av", "--delete"]
116
132
    if ssh:
117
133
        cmd.extend(('-e', 'ssh'))
118
 
    for exclude in exclude_globs:
119
 
        cmd.extend(('--exclude', exclude))
 
134
    if len(excludes) > 0:
 
135
        cmd.extend(('--exclude-from', '-'))
120
136
    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):
 
137
    if silent:
 
138
        stderr = PIPE
 
139
        stdout = PIPE
 
140
    else:
 
141
        stderr = None
 
142
        stdout = None
 
143
    try:
 
144
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
145
    except OSError, e:
 
146
        if e.errno == errno.ENOENT:
 
147
            raise NoRsync(rsync_name)
 
148
            
 
149
    proc.stdin.write('\n'.join(excludes)+'\n')
 
150
    proc.stdin.close()
 
151
    if silent:
 
152
        proc.stderr.read()
 
153
        proc.stderr.close()
 
154
        proc.stdout.read()
 
155
        proc.stdout.close()
 
156
    proc.wait()
 
157
    if proc.returncode == 12:
 
158
        raise RsyncStreamIO()
 
159
    elif proc.returncode == 23:
 
160
        raise RsyncNoFile(source)
 
161
    elif proc.returncode != 0:
 
162
        raise RsyncUnknownStatus(proc.returncode)
 
163
    return cmd
 
164
 
 
165
 
 
166
def rsync_ls(source, ssh=False, silent=True):
 
167
    cmd = ["rsync"]
 
168
    if ssh:
 
169
        cmd.extend(('-e', 'ssh'))
 
170
    cmd.append(source)
 
171
    if silent:
 
172
        stderr = PIPE
 
173
    else:
 
174
        stderr = None
 
175
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
176
    result = proc.stdout.read()
 
177
    proc.stdout.close()
 
178
    if silent:
 
179
        proc.stderr.read()
 
180
        proc.stderr.close()
 
181
    proc.wait()
 
182
    if proc.returncode == 12:
 
183
        raise RsyncStreamIO()
 
184
    elif proc.returncode == 23:
 
185
        raise RsyncNoFile(source)
 
186
    elif proc.returncode != 0:
 
187
        raise RsyncUnknownStatus(proc.returncode)
 
188
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
189
 
 
190
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
 
191
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
 
192
              '.bzr/x-rsync-data')
 
193
 
 
194
 
 
195
def read_revision_history(fname):
 
196
    return [l.rstrip('\r\n') for l in
 
197
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
198
 
 
199
class RsyncNoFile(Exception):
 
200
    def __init__(self, path):
 
201
        Exception.__init__(self, "No such file %s" % path)
 
202
 
 
203
class RsyncStreamIO(Exception):
 
204
    def __init__(self):
 
205
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
206
 
 
207
def get_revision_history(location):
 
208
    tempdir = tempfile.mkdtemp('push')
 
209
    try:
 
210
        history_fname = os.path.join(tempdir, 'revision-history')
 
211
        cmd = rsync(location+'.bzr/revision-history', history_fname,
 
212
                    silent=True)
 
213
        history = read_revision_history(history_fname)
 
214
    finally:
 
215
        shutil.rmtree(tempdir)
 
216
    return history
 
217
 
 
218
def history_subset(location, branch):
 
219
    remote_history = get_revision_history(location)
 
220
    local_history = branch.revision_history()
 
221
    if len(remote_history) > len(local_history):
 
222
        return False
 
223
    for local, remote in zip(remote_history, local_history):
 
224
        if local != remote:
 
225
            return False 
 
226
    return True
 
227
 
 
228
def empty_or_absent(location):
 
229
    try:
 
230
        files = rsync_ls(location)
 
231
        return files == ['.']
 
232
    except RsyncNoFile:
 
233
        return True
 
234
 
 
235
def push(cur_branch, location=None, overwrite=False, working_tree=True):
152
236
    push_location = get_push_data(cur_branch)
153
237
    if location is not None:
154
238
        if not location.endswith('/'):
156
240
        push_location = location
157
241
    
158
242
    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
 
 
 
243
        raise bzrlib.errors.MustUseDecorated
 
244
 
 
245
    if push_location.find('://') != -1:
 
246
        raise bzrlib.errors.MustUseDecorated
 
247
 
 
248
    if push_location.find(':') == -1:
 
249
        raise bzrlib.errors.MustUseDecorated
 
250
 
 
251
    clean, non_source = is_clean(cur_branch)
 
252
    if not clean:
 
253
        print """Error: This tree has uncommitted changes or unknown (?) files.
 
254
Use "bzr status" to list them."""
 
255
        sys.exit(1)
 
256
    if working_tree:
 
257
        final_exclusions = non_source[:]
 
258
    else:
 
259
        wt = cur_branch.working_tree()
 
260
        final_exclusions = []
 
261
        for path, status, kind, file_id, entry in wt.list_files():
 
262
            final_exclusions.append(path)
 
263
 
 
264
    final_exclusions.extend(exclusions)
 
265
    if not overwrite:
 
266
        try:
 
267
            if not history_subset(push_location, cur_branch):
 
268
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
269
                                                    " newer version of remote"
 
270
                                                    " branch.")
 
271
        except RsyncNoFile:
 
272
            if not empty_or_absent(push_location):
 
273
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
274
                                                    " bzr branch (or empty"
 
275
                                                    " directory)")
 
276
        except RsyncStreamIO:
 
277
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
278
                " specified location.  Please ensure that"
 
279
                ' "%s" is of the form "machine:/path".' % push_location)
166
280
    print "Pushing to %s" % push_location
167
 
    rsync(cur_branch.base+'/', push_location, ssh=True,
168
 
          exclude_globs=exclusions)
 
281
    rsync(cur_branch.base+'/', push_location, ssh=True, 
 
282
          excludes=final_exclusions)
169
283
 
170
284
    set_push_data(cur_branch, push_location)
171
285
 
 
286
def short_committer(committer):
 
287
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
288
    if len(new_committer) < 2:
 
289
        return committer
 
290
    return new_committer
 
291
 
 
292
 
172
293
def run_tests():
173
294
    import doctest
174
295
    result = doctest.testmod()