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