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