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