~abentley/bzrtools/bzrtools.dev

89 by Aaron Bentley
Added copyright/GPL notices
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
313 by Aaron Bentley
Updated to match API changes
17
import codecs
18
import errno
19 by abentley
librified most of the pull script
19
import os
313 by Aaron Bentley
Updated to match API changes
20
import re
16 by abentley
Got is_clean under test, added setters/getters for pull data
21
import tempfile
22
import shutil
117 by aaron.bentley at utoronto
Excluded non-source files
23
from subprocess import Popen, PIPE
313 by Aaron Bentley
Updated to match API changes
24
import sys
25
26
import bzrlib
27
import bzrlib.errors
28
from bzrlib.bzrdir import BzrDir
29
30
def temp_tree():
16 by abentley
Got is_clean under test, added setters/getters for pull data
31
    dirname = tempfile.mkdtemp("temp-branch")
313 by Aaron Bentley
Updated to match API changes
32
    return BzrDir.create_standalone_workingtree(dirname)
33
34
def rm_tree(tree):
35
    shutil.rmtree(tree.basedir)
36
37
def is_clean(cur_tree):
16 by abentley
Got is_clean under test, added setters/getters for pull data
38
    """
39
    Return true if no files are modifed or unknown
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
40
    >>> import bzrlib.add
313 by Aaron Bentley
Updated to match API changes
41
    >>> tree = temp_tree()
42
    >>> is_clean(tree)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
43
    (True, [])
313 by Aaron Bentley
Updated to match API changes
44
    >>> fooname = os.path.join(tree.basedir, "foo")
16 by abentley
Got is_clean under test, added setters/getters for pull data
45
    >>> file(fooname, "wb").write("bar")
313 by Aaron Bentley
Updated to match API changes
46
    >>> is_clean(tree)
239 by Aaron Bentley
Fixed test case
47
    (True, [u'foo'])
313 by Aaron Bentley
Updated to match API changes
48
    >>> bzrlib.add.smart_add_tree(tree, [tree.basedir])
302 by Aaron Bentley
Updated test to match bzrlib
49
    ([u'foo'], {})
313 by Aaron Bentley
Updated to match API changes
50
    >>> is_clean(tree)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
51
    (False, [])
313 by Aaron Bentley
Updated to match API changes
52
    >>> tree.commit("added file")
53
    >>> is_clean(tree)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
54
    (True, [])
313 by Aaron Bentley
Updated to match API changes
55
    >>> rm_tree(tree)
16 by abentley
Got is_clean under test, added setters/getters for pull data
56
    """
95 by Aaron Bentley
Updated to use compare_trees directly from diff
57
    from bzrlib.diff import compare_trees
313 by Aaron Bentley
Updated to match API changes
58
    old_tree = cur_tree.basis_tree()
59
    new_tree = cur_tree
117 by aaron.bentley at utoronto
Excluded non-source files
60
    non_source = []
209 by Aaron Bentley
updated to match Tree.list_files sig change
61
    for path, file_class, kind, file_id, entry in new_tree.list_files():
117 by aaron.bentley at utoronto
Excluded non-source files
62
        if file_class in ('?', 'I'):
63
            non_source.append(path)
95 by Aaron Bentley
Updated to use compare_trees directly from diff
64
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
257.1.3 by Aaron Bentley
Switched to TreeDelta.has_changed
65
    return not delta.has_changed(), non_source
16 by abentley
Got is_clean under test, added setters/getters for pull data
66
313 by Aaron Bentley
Updated to match API changes
67
def set_push_data(tree, location):
68
    push_file = file (tree._control_files.controlfilename("x-push-data"), "wb")
20 by abentley
added bzr-push command
69
    push_file.write("%s\n" % location)
70
313 by Aaron Bentley
Updated to match API changes
71
def get_push_data(tree):
20 by abentley
added bzr-push command
72
    """
313 by Aaron Bentley
Updated to match API changes
73
    >>> tree = temp_tree()
74
    >>> get_push_data(tree) is None
20 by abentley
added bzr-push command
75
    True
313 by Aaron Bentley
Updated to match API changes
76
    >>> set_push_data(tree, 'http://somewhere')
77
    >>> get_push_data(tree)
20 by abentley
added bzr-push command
78
    'http://somewhere'
313 by Aaron Bentley
Updated to match API changes
79
    >>> rm_tree(tree)
20 by abentley
added bzr-push command
80
    """
313 by Aaron Bentley
Updated to match API changes
81
    filename = tree._control_files.controlfilename("x-push-data")
20 by abentley
added bzr-push command
82
    if not os.path.exists(filename):
83
        return None
84
    push_file = file (filename, "rb")
85
    (location,) = [f.rstrip('\n') for f in push_file]
86
    return location
87
19 by abentley
librified most of the pull script
88
"""
89
>>> shell_escape('hello')
90
'\h\e\l\l\o'
91
"""
92
def shell_escape(arg):
93
    return "".join(['\\'+c for c in arg])
94
95
def safe_system(args):
96
    """
97
    >>> real_system = os.system
98
    >>> os.system = sys.stdout.write
99
    >>> safe_system(['a', 'b', 'cd'])
100
    \\a \\b \\c\\d
101
    >>> os.system = real_system
102
    """
103
    arg_str = " ".join([shell_escape(a) for a in args])
104
    return os.system(arg_str)
105
195 by Aaron Bentley
prevented accidental overwrites from push
106
class RsyncUnknownStatus(Exception):
107
    def __init__(self, status):
108
        Exception.__init__(self, "Unknown status: %d" % status)
109
199 by Aaron Bentley
Updated doctests
110
class NoRsync(Exception):
111
    def __init__(self, rsync_name):
112
        Exception.__init__(self, "%s not found." % rsync_name)
113
114
def rsync(source, target, ssh=False, excludes=(), silent=False, 
115
          rsync_name="rsync"):
19 by abentley
librified most of the pull script
116
    """
311 by Aaron Bentley
Updated rsync test to make no assumptions about cwd
117
    >>> new_dir = tempfile.mkdtemp()
118
    >>> old_dir = os.getcwd()
119
    >>> os.chdir(new_dir)
198 by Aaron Bentley
Updated doctests
120
    >>> rsync("a", "b", silent=True)
121
    Traceback (most recent call last):
122
    RsyncNoFile: No such file a
123
    >>> rsync("a", "b", excludes=("*.py",), silent=True)
124
    Traceback (most recent call last):
125
    RsyncNoFile: No such file a
199 by Aaron Bentley
Updated doctests
126
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
127
    Traceback (most recent call last):
128
    NoRsync: rsyncc not found.
311 by Aaron Bentley
Updated rsync test to make no assumptions about cwd
129
    >>> os.chdir(old_dir)
130
    >>> os.rmdir(new_dir)
19 by abentley
librified most of the pull script
131
    """
199 by Aaron Bentley
Updated doctests
132
    cmd = [rsync_name, "-av", "--delete"]
20 by abentley
added bzr-push command
133
    if ssh:
134
        cmd.extend(('-e', 'ssh'))
117 by aaron.bentley at utoronto
Excluded non-source files
135
    if len(excludes) > 0:
136
        cmd.extend(('--exclude-from', '-'))
20 by abentley
added bzr-push command
137
    cmd.extend((source, target))
195 by Aaron Bentley
prevented accidental overwrites from push
138
    if silent:
139
        stderr = PIPE
140
        stdout = PIPE
141
    else:
142
        stderr = None
143
        stdout = None
199 by Aaron Bentley
Updated doctests
144
    try:
145
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
146
    except OSError, e:
147
        if e.errno == errno.ENOENT:
148
            raise NoRsync(rsync_name)
149
            
117 by aaron.bentley at utoronto
Excluded non-source files
150
    proc.stdin.write('\n'.join(excludes)+'\n')
151
    proc.stdin.close()
195 by Aaron Bentley
prevented accidental overwrites from push
152
    if silent:
153
        proc.stderr.read()
154
        proc.stderr.close()
155
        proc.stdout.read()
156
        proc.stdout.close()
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
157
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
158
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
159
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
160
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
161
        raise RsyncNoFile(source)
162
    elif proc.returncode != 0:
163
        raise RsyncUnknownStatus(proc.returncode)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
164
    return cmd
117 by aaron.bentley at utoronto
Excluded non-source files
165
195 by Aaron Bentley
prevented accidental overwrites from push
166
167
def rsync_ls(source, ssh=False, silent=True):
168
    cmd = ["rsync"]
169
    if ssh:
170
        cmd.extend(('-e', 'ssh'))
171
    cmd.append(source)
172
    if silent:
173
        stderr = PIPE
174
    else:
175
        stderr = None
176
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
177
    result = proc.stdout.read()
178
    proc.stdout.close()
179
    if silent:
180
        proc.stderr.read()
181
        proc.stderr.close()
182
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
183
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
184
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
185
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
186
        raise RsyncNoFile(source)
187
    elif proc.returncode != 0:
188
        raise RsyncUnknownStatus(proc.returncode)
189
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
190
195.1.1 by Aaron Bentley
Added more file exclusions
191
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
195.1.2 by Aaron Bentley
Added more exclusions
192
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
193
              '.bzr/x-rsync-data')
19 by abentley
librified most of the pull script
194
20 by abentley
added bzr-push command
195
195 by Aaron Bentley
prevented accidental overwrites from push
196
def read_revision_history(fname):
197
    return [l.rstrip('\r\n') for l in
198
            codecs.open(fname, 'rb', 'utf-8').readlines()]
199
200
class RsyncNoFile(Exception):
201
    def __init__(self, path):
202
        Exception.__init__(self, "No such file %s" % path)
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
203
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
204
class RsyncStreamIO(Exception):
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
205
    def __init__(self):
206
        Exception.__init__(self, "Error in rsync protocol data stream.")
195 by Aaron Bentley
prevented accidental overwrites from push
207
208
def get_revision_history(location):
209
    tempdir = tempfile.mkdtemp('push')
210
    try:
211
        history_fname = os.path.join(tempdir, 'revision-history')
212
        cmd = rsync(location+'.bzr/revision-history', history_fname,
213
                    silent=True)
214
        history = read_revision_history(history_fname)
215
    finally:
216
        shutil.rmtree(tempdir)
217
    return history
218
219
def history_subset(location, branch):
220
    remote_history = get_revision_history(location)
221
    local_history = branch.revision_history()
222
    if len(remote_history) > len(local_history):
223
        return False
224
    for local, remote in zip(remote_history, local_history):
225
        if local != remote:
226
            return False 
227
    return True
228
229
def empty_or_absent(location):
230
    try:
231
        files = rsync_ls(location)
232
        return files == ['.']
233
    except RsyncNoFile:
234
        return True
235
313 by Aaron Bentley
Updated to match API changes
236
def push(tree, location=None, overwrite=False, working_tree=True):
237
    push_location = get_push_data(tree)
20 by abentley
added bzr-push command
238
    if location is not None:
25 by abentley
fixed push for x files, tracefile, push/pull miscommunication
239
        if not location.endswith('/'):
240
            location += '/'
20 by abentley
added bzr-push command
241
        push_location = location
242
    
243
    if push_location is None:
271 by Aaron Bentley
Cherry-picked Robert's diff and push fixes
244
        raise bzrlib.errors.MustUseDecorated
245
246
    if push_location.find('://') != -1:
247
        raise bzrlib.errors.MustUseDecorated
248
249
    if push_location.find(':') == -1:
250
        raise bzrlib.errors.MustUseDecorated
20 by abentley
added bzr-push command
251
313 by Aaron Bentley
Updated to match API changes
252
    clean, non_source = is_clean(tree)
117 by aaron.bentley at utoronto
Excluded non-source files
253
    if not clean:
88 by Aaron Bentley
Added suggestion to use 'bzr status' to push error.
254
        print """Error: This tree has uncommitted changes or unknown (?) files.
255
Use "bzr status" to list them."""
20 by abentley
added bzr-push command
256
        sys.exit(1)
303 by Aaron Bentley
Added support for pushing with no working tree
257
    if working_tree:
258
        final_exclusions = non_source[:]
259
    else:
313 by Aaron Bentley
Updated to match API changes
260
        wt = tree
303 by Aaron Bentley
Added support for pushing with no working tree
261
        final_exclusions = []
262
        for path, status, kind, file_id, entry in wt.list_files():
263
            final_exclusions.append(path)
264
265
    final_exclusions.extend(exclusions)
195 by Aaron Bentley
prevented accidental overwrites from push
266
    if not overwrite:
267
        try:
313 by Aaron Bentley
Updated to match API changes
268
            if not history_subset(push_location, tree.branch):
195 by Aaron Bentley
prevented accidental overwrites from push
269
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
270
                                                    " newer version of remote"
271
                                                    " branch.")
272
        except RsyncNoFile:
273
            if not empty_or_absent(push_location):
274
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
275
                                                    " bzr branch (or empty"
276
                                                    " directory)")
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
277
        except RsyncStreamIO:
278
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
279
                " specified location.  Please ensure that"
280
                ' "%s" is of the form "machine:/path".' % push_location)
20 by abentley
added bzr-push command
281
    print "Pushing to %s" % push_location
313 by Aaron Bentley
Updated to match API changes
282
    rsync(tree.bzrdir.transport.base+'/', push_location, ssh=True, 
303 by Aaron Bentley
Added support for pushing with no working tree
283
          excludes=final_exclusions)
20 by abentley
added bzr-push command
284
315 by Aaron Bentley
Fixed last step of push
285
    set_push_data(tree, push_location)
20 by abentley
added bzr-push command
286
292 by Aaron Bentley
Introduced branch-history command
287
def short_committer(committer):
288
    new_committer = re.sub('<.*>', '', committer).strip(' ')
289
    if len(new_committer) < 2:
290
        return committer
291
    return new_committer
292
293
19 by abentley
librified most of the pull script
294
def run_tests():
16 by abentley
Got is_clean under test, added setters/getters for pull data
295
    import doctest
18 by abentley
Finished implementing bzr-pull
296
    result = doctest.testmod()
19 by abentley
librified most of the pull script
297
    if result[1] > 0:
298
        if result[0] == 0:
299
            print "All tests passed"
300
    else:
301
        print "No tests to run"
302
if __name__ == "__main__":
303
    run_tests()