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