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