~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):
69
    push_file = file (tree._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
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
82
    filename = tree._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
195.1.1 by Aaron Bentley
Added more file exclusions
192
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
195.1.2 by Aaron Bentley
Added more exclusions
193
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
322 by Aaron Bentley
Tweaked the ignore list slightly
194
              '.bzr/x-rsync-data', '.bzr/basis-inventory', 
195
              '.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')
214
        cmd = rsync(location+'.bzr/revision-history', history_fname,
215
                    silent=True)
216
        history = read_revision_history(history_fname)
217
    finally:
218
        shutil.rmtree(tempdir)
219
    return history
220
221
def history_subset(location, branch):
222
    remote_history = get_revision_history(location)
223
    local_history = branch.revision_history()
224
    if len(remote_history) > len(local_history):
225
        return False
226
    for local, remote in zip(remote_history, local_history):
227
        if local != remote:
228
            return False 
229
    return True
230
231
def empty_or_absent(location):
232
    try:
233
        files = rsync_ls(location)
234
        return files == ['.']
235
    except RsyncNoFile:
236
        return True
237
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
238
def push(tree, location=None, overwrite=False, working_tree=True):
239
    push_location = get_push_data(tree)
20 by abentley
added bzr-push command
240
    if location is not None:
25 by abentley
fixed push for x files, tracefile, push/pull miscommunication
241
        if not location.endswith('/'):
242
            location += '/'
20 by abentley
added bzr-push command
243
        push_location = location
244
    
245
    if push_location is None:
321 by Aaron Bentley
Only use the decorated push if it's likely to work.
246
        if tree.branch.get_push_location() is None:
247
            raise BzrCommandError("No push location known or specified.")
248
        else:
249
            raise bzrlib.errors.MustUseDecorated
271 by Aaron Bentley
Cherry-picked Robert's diff and push fixes
250
251
    if push_location.find('://') != -1:
252
        raise bzrlib.errors.MustUseDecorated
253
254
    if push_location.find(':') == -1:
255
        raise bzrlib.errors.MustUseDecorated
20 by abentley
added bzr-push command
256
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
257
    clean, non_source = is_clean(tree)
117 by aaron.bentley at utoronto
Excluded non-source files
258
    if not clean:
88 by Aaron Bentley
Added suggestion to use 'bzr status' to push error.
259
        print """Error: This tree has uncommitted changes or unknown (?) files.
260
Use "bzr status" to list them."""
20 by abentley
added bzr-push command
261
        sys.exit(1)
303 by Aaron Bentley
Added support for pushing with no working tree
262
    if working_tree:
263
        final_exclusions = non_source[:]
264
    else:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
265
        wt = tree
303 by Aaron Bentley
Added support for pushing with no working tree
266
        final_exclusions = []
267
        for path, status, kind, file_id, entry in wt.list_files():
268
            final_exclusions.append(path)
269
270
    final_exclusions.extend(exclusions)
195 by Aaron Bentley
prevented accidental overwrites from push
271
    if not overwrite:
272
        try:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
273
            if not history_subset(push_location, tree.branch):
195 by Aaron Bentley
prevented accidental overwrites from push
274
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
275
                                                    " newer version of remote"
276
                                                    " branch.")
277
        except RsyncNoFile:
278
            if not empty_or_absent(push_location):
279
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
280
                                                    " bzr branch (or empty"
281
                                                    " directory)")
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
282
        except RsyncStreamIO:
283
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
284
                " specified location.  Please ensure that"
285
                ' "%s" is of the form "machine:/path".' % push_location)
20 by abentley
added bzr-push command
286
    print "Pushing to %s" % push_location
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
287
    rsync(tree.basedir+'/', push_location, ssh=True, 
303 by Aaron Bentley
Added support for pushing with no working tree
288
          excludes=final_exclusions)
20 by abentley
added bzr-push command
289
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
290
    set_push_data(tree, push_location)
20 by abentley
added bzr-push command
291
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
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()