~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-03-07 17:23:04 UTC
  • mto: (147.1.71 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 323.
  • Revision ID: abentley@panoramicfeedback.com-20060307172304-81a37ded2450ce99
Fixed up Robert's test changes

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