~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
    """
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)
423.1.3 by Aaron Bentley
Update to match 0.9 api
67
    delta = new_tree.changes_from(old_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):
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
71
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
20 by abentley
added bzr-push command
72
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
73
def get_push_data(tree):
20 by abentley
added bzr-push command
74
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
75
    >>> tree = temp_tree()
76
    >>> get_push_data(tree) is None
20 by abentley
added bzr-push command
77
    True
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
78
    >>> set_push_data(tree, 'http://somewhere')
79
    >>> get_push_data(tree)
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
80
    u'http://somewhere'
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
81
    >>> rm_tree(tree)
20 by abentley
added bzr-push command
82
    """
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
83
    try:
84
        location = tree.branch.control_files.get_utf8('x-push-data').read()
85
    except NoSuchFile:
20 by abentley
added bzr-push command
86
        return None
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
87
    return location.rstrip('\n')
20 by abentley
added bzr-push command
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
364.1.4 by Aaron Bentley
Changed rpush to rspush
242
def rspush(tree, location=None, overwrite=False, working_tree=True):
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
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:
364.1.4 by Aaron Bentley
Changed rpush to rspush
250
        raise BzrCommandError("No rspush location known or specified.")
147.4.37 by Robert Collins
Convert push to rpush.
251
252
    if (push_location.find('://') != -1 or
253
        push_location.find(':') == -1):
254
        raise BzrCommandError("Invalid rsync path %r." % push_location)
20 by abentley
added bzr-push command
255
303 by Aaron Bentley
Added support for pushing with no working tree
256
    if working_tree:
333 by Aaron Bentley
Only error on dirty wt if we are pushing the wt
257
        clean, non_source = is_clean(tree)
258
        if not clean:
259
            print """Error: This tree has uncommitted changes or unknown (?) files.
260
    Use "bzr status" to list them."""
261
            sys.exit(1)
303 by Aaron Bentley
Added support for pushing with no working tree
262
        final_exclusions = non_source[:]
263
    else:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
264
        wt = tree
303 by Aaron Bentley
Added support for pushing with no working tree
265
        final_exclusions = []
266
        for path, status, kind, file_id, entry in wt.list_files():
267
            final_exclusions.append(path)
268
269
    final_exclusions.extend(exclusions)
195 by Aaron Bentley
prevented accidental overwrites from push
270
    if not overwrite:
271
        try:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
272
            if not history_subset(push_location, tree.branch):
195 by Aaron Bentley
prevented accidental overwrites from push
273
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
274
                                                    " newer version of remote"
275
                                                    " branch.")
276
        except RsyncNoFile:
277
            if not empty_or_absent(push_location):
278
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
279
                                                    " bzr branch (or empty"
280
                                                    " directory)")
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
281
        except RsyncStreamIO:
282
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
283
                " specified location.  Please ensure that"
284
                ' "%s" is of the form "machine:/path".' % push_location)
20 by abentley
added bzr-push command
285
    print "Pushing to %s" % push_location
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
286
    rsync(tree.basedir+'/', push_location, ssh=True, 
303 by Aaron Bentley
Added support for pushing with no working tree
287
          excludes=final_exclusions)
20 by abentley
added bzr-push command
288
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
289
    set_push_data(tree, push_location)
20 by abentley
added bzr-push command
290
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
291
292 by Aaron Bentley
Introduced branch-history command
292
def short_committer(committer):
293
    new_committer = re.sub('<.*>', '', committer).strip(' ')
294
    if len(new_committer) < 2:
295
        return committer
296
    return new_committer
297
298
354 by Aaron Bentley
Added apache index scraping to the branches command
299
def apache_ls(t):
300
    """Screen-scrape Apache listings"""
355 by Aaron Bentley
Handled more hrefs
301
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
302
        ' <a href="'
354 by Aaron Bentley
Added apache index scraping to the branches command
303
    lines = t.get('.')
355 by Aaron Bentley
Handled more hrefs
304
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
354 by Aaron Bentley
Added apache index scraping to the branches command
305
    for line in lines:
355 by Aaron Bentley
Handled more hrefs
306
        match = expr.search(line)
307
        if match is None:
308
            continue
309
        url = match.group(1)
310
        if url.startswith('http://') or url.startswith('/') or '../' in url:
311
            continue
357 by Aaron Bentley
Tweakage of the apache scraper
312
        if '?' in url:
313
            continue
355 by Aaron Bentley
Handled more hrefs
314
        yield url.rstrip('/')
354 by Aaron Bentley
Added apache index scraping to the branches command
315
316
317
def iter_branches(t, lister=None):
352 by Aaron Bentley
Added branches subcommand
318
    """Iterate through all the branches under a transport"""
354 by Aaron Bentley
Added apache index scraping to the branches command
319
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
320
        try:
321
            branch = bzrdir.open_branch()
322
            if branch.bzrdir is bzrdir:
323
                yield branch
324
        except (NotBranchError, UnsupportedFormatError):
325
            pass
326
354 by Aaron Bentley
Added apache index scraping to the branches command
327
328
def iter_branch_tree(t, lister=None):
329
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
330
        try:
331
            wt = bzrdir.open_workingtree()
332
            yield wt.branch, wt
333
        except NoWorkingTree, UnsupportedFormatError:
334
            try:
335
                branch = bzrdir.open_branch()
336
                if branch.bzrdir is bzrdir:
337
                    yield branch, None
338
            except (NotBranchError, UnsupportedFormatError):
339
                continue
340
356 by Aaron Bentley
Fixed robustness issues in branches command
341
354 by Aaron Bentley
Added apache index scraping to the branches command
342
def iter_bzrdirs(t, lister=None):
343
    if lister is None:
344
        def lister(t):
345
            return t.list_dir('.')
352 by Aaron Bentley
Added branches subcommand
346
    try:
347
        bzrdir = bzrdir_from_transport(t)
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
348
        yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
349
    except (NotBranchError, UnsupportedFormatError, TransportError,
350
            PermissionDenied):
352 by Aaron Bentley
Added branches subcommand
351
        pass
352
    try:
354 by Aaron Bentley
Added apache index scraping to the branches command
353
        for directory in lister(t):
352 by Aaron Bentley
Added branches subcommand
354
            if directory == ".bzr":
355
                continue
356 by Aaron Bentley
Fixed robustness issues in branches command
356
            try:
357
                subt = t.clone(directory)
358
            except UnicodeDecodeError:
359
                continue
357 by Aaron Bentley
Tweakage of the apache scraper
360
            for bzrdir in iter_bzrdirs(subt, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
361
                yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
362
    except (NoSuchFile, PermissionDenied, TransportError):
352 by Aaron Bentley
Added branches subcommand
363
        pass
364
365
    
366
def bzrdir_from_transport(t):
367
    """Open a bzrdir from a transport (not a location)"""
368
    format = BzrDirFormat.find_format(t)
369
    BzrDir._check_supported(format, False)
370
    return format.open(t)
371
372
19 by abentley
librified most of the pull script
373
def run_tests():
16 by abentley
Got is_clean under test, added setters/getters for pull data
374
    import doctest
18 by abentley
Finished implementing bzr-pull
375
    result = doctest.testmod()
19 by abentley
librified most of the pull script
376
    if result[1] > 0:
377
        if result[0] == 0:
378
            print "All tests passed"
379
    else:
380
        print "No tests to run"
381
if __name__ == "__main__":
382
    run_tests()