~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)
42 by Aaron Bentley
Got tree check working with new API
63
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
64
        len(delta.modified) > 0:
117 by aaron.bentley at utoronto
Excluded non-source files
65
        return False, non_source
66
    return True, non_source 
16 by abentley
Got is_clean under test, added setters/getters for pull data
67
68
def set_pull_data(br, location, rev_id):
19 by abentley
librified most of the pull script
69
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
16 by abentley
Got is_clean under test, added setters/getters for pull data
70
    pull_file.write("%s\n%s\n" % (location, rev_id))
71
72
def get_pull_data(br):
73
    """
74
    >>> br = temp_branch()
75
    >>> get_pull_data(br)
76
    (None, None)
77
    >>> set_pull_data(br, 'http://somewhere', '888-777')
78
    >>> get_pull_data(br)
79
    ('http://somewhere', '888-777')
80
    >>> rm_branch(br)
81
    """
19 by abentley
librified most of the pull script
82
    filename = br.controlfilename("x-pull-data")
16 by abentley
Got is_clean under test, added setters/getters for pull data
83
    if not os.path.exists(filename):
84
        return (None, None)
85
    pull_file = file (filename, "rb")
86
    location, rev_id = [f.rstrip('\n') for f in pull_file]
87
    return location, rev_id
88
20 by abentley
added bzr-push command
89
def set_push_data(br, location):
90
    push_file = file (br.controlfilename("x-push-data"), "wb")
91
    push_file.write("%s\n" % location)
92
93
def get_push_data(br):
94
    """
95
    >>> br = temp_branch()
96
    >>> get_push_data(br) is None
97
    True
98
    >>> set_push_data(br, 'http://somewhere')
99
    >>> get_push_data(br)
100
    'http://somewhere'
101
    >>> rm_branch(br)
102
    """
103
    filename = br.controlfilename("x-push-data")
104
    if not os.path.exists(filename):
105
        return None
106
    push_file = file (filename, "rb")
107
    (location,) = [f.rstrip('\n') for f in push_file]
108
    return location
109
19 by abentley
librified most of the pull script
110
"""
111
>>> shell_escape('hello')
112
'\h\e\l\l\o'
113
"""
114
def shell_escape(arg):
115
    return "".join(['\\'+c for c in arg])
116
117
def safe_system(args):
118
    """
119
    >>> real_system = os.system
120
    >>> os.system = sys.stdout.write
121
    >>> safe_system(['a', 'b', 'cd'])
122
    \\a \\b \\c\\d
123
    >>> os.system = real_system
124
    """
125
    arg_str = " ".join([shell_escape(a) for a in args])
126
    return os.system(arg_str)
127
195 by Aaron Bentley
prevented accidental overwrites from push
128
class RsyncUnknownStatus(Exception):
129
    def __init__(self, status):
130
        Exception.__init__(self, "Unknown status: %d" % status)
131
199 by Aaron Bentley
Updated doctests
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"):
19 by abentley
librified most of the pull script
138
    """
198 by Aaron Bentley
Updated doctests
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
199 by Aaron Bentley
Updated doctests
145
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
146
    Traceback (most recent call last):
147
    NoRsync: rsyncc not found.
19 by abentley
librified most of the pull script
148
    """
199 by Aaron Bentley
Updated doctests
149
    cmd = [rsync_name, "-av", "--delete"]
20 by abentley
added bzr-push command
150
    if ssh:
151
        cmd.extend(('-e', 'ssh'))
117 by aaron.bentley at utoronto
Excluded non-source files
152
    if len(excludes) > 0:
153
        cmd.extend(('--exclude-from', '-'))
20 by abentley
added bzr-push command
154
    cmd.extend((source, target))
195 by Aaron Bentley
prevented accidental overwrites from push
155
    if silent:
156
        stderr = PIPE
157
        stdout = PIPE
158
    else:
159
        stderr = None
160
        stdout = None
199 by Aaron Bentley
Updated doctests
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
            
117 by aaron.bentley at utoronto
Excluded non-source files
167
    proc.stdin.write('\n'.join(excludes)+'\n')
168
    proc.stdin.close()
195 by Aaron Bentley
prevented accidental overwrites from push
169
    if silent:
170
        proc.stderr.read()
171
        proc.stderr.close()
172
        proc.stdout.read()
173
        proc.stdout.close()
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
174
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
175
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
176
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
177
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
178
        raise RsyncNoFile(source)
179
    elif proc.returncode != 0:
180
        raise RsyncUnknownStatus(proc.returncode)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
181
    return cmd
117 by aaron.bentley at utoronto
Excluded non-source files
182
195 by Aaron Bentley
prevented accidental overwrites from push
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.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
200
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
201
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
202
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
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
195.1.1 by Aaron Bentley
Added more file exclusions
208
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
195.1.2 by Aaron Bentley
Added more exclusions
209
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
210
              '.bzr/x-rsync-data')
19 by abentley
librified most of the pull script
211
20 by abentley
added bzr-push command
212
195 by Aaron Bentley
prevented accidental overwrites from push
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)
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
220
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
221
class RsyncStreamIO(Exception):
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
222
    def __init__(self):
223
        Exception.__init__(self, "Error in rsync protocol data stream.")
195 by Aaron Bentley
prevented accidental overwrites from push
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):
20 by abentley
added bzr-push command
254
    push_location = get_push_data(cur_branch)
255
    if location is not None:
25 by abentley
fixed push for x files, tracefile, push/pull miscommunication
256
        if not location.endswith('/'):
257
            location += '/'
20 by abentley
added bzr-push command
258
        push_location = location
259
    
260
    if push_location is None:
261
        print "No push location saved.  Please specify one on the command line."
262
        sys.exit(1)
263
117 by aaron.bentley at utoronto
Excluded non-source files
264
    clean, non_source = is_clean(cur_branch)
265
    if not clean:
88 by Aaron Bentley
Added suggestion to use 'bzr status' to push error.
266
        print """Error: This tree has uncommitted changes or unknown (?) files.
267
Use "bzr status" to list them."""
20 by abentley
added bzr-push command
268
        sys.exit(1)
117 by aaron.bentley at utoronto
Excluded non-source files
269
    non_source.extend(exclusions)
195 by Aaron Bentley
prevented accidental overwrites from push
270
    if not overwrite:
271
        try:
272
            if not history_subset(push_location, cur_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)")
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
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)
20 by abentley
added bzr-push command
285
    print "Pushing to %s" % push_location
117 by aaron.bentley at utoronto
Excluded non-source files
286
    rsync(cur_branch.base+'/', push_location, ssh=True, excludes=non_source)
20 by abentley
added bzr-push command
287
288
    set_push_data(cur_branch, push_location)
289
19 by abentley
librified most of the pull script
290
def run_tests():
16 by abentley
Got is_clean under test, added setters/getters for pull data
291
    import doctest
18 by abentley
Finished implementing bzr-pull
292
    result = doctest.testmod()
19 by abentley
librified most of the pull script
293
    if result[1] > 0:
294
        if result[0] == 0:
295
            print "All tests passed"
296
    else:
297
        print "No tests to run"
298
if __name__ == "__main__":
299
    run_tests()