~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

Update for integration move of read_working_inventory from Branch to WorkingTree.

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