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