~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
472 by Aaron Bentley
Make rspush work with rsync servers (Tridge)
252
    if (push_location.find('::') != -1):
253
        usessh=False
254
    else:
255
        usessh=True
256
147.4.37 by Robert Collins
Convert push to rpush.
257
    if (push_location.find('://') != -1 or
258
        push_location.find(':') == -1):
259
        raise BzrCommandError("Invalid rsync path %r." % push_location)
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
472 by Aaron Bentley
Make rspush work with rsync servers (Tridge)
291
    rsync(tree.basedir+'/', push_location, ssh=usessh, 
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
354 by Aaron Bentley
Added apache index scraping to the branches command
304
def apache_ls(t):
305
    """Screen-scrape Apache listings"""
355 by Aaron Bentley
Handled more hrefs
306
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
307
        ' <a href="'
354 by Aaron Bentley
Added apache index scraping to the branches command
308
    lines = t.get('.')
355 by Aaron Bentley
Handled more hrefs
309
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
354 by Aaron Bentley
Added apache index scraping to the branches command
310
    for line in lines:
355 by Aaron Bentley
Handled more hrefs
311
        match = expr.search(line)
312
        if match is None:
313
            continue
314
        url = match.group(1)
315
        if url.startswith('http://') or url.startswith('/') or '../' in url:
316
            continue
357 by Aaron Bentley
Tweakage of the apache scraper
317
        if '?' in url:
318
            continue
355 by Aaron Bentley
Handled more hrefs
319
        yield url.rstrip('/')
354 by Aaron Bentley
Added apache index scraping to the branches command
320
321
322
def iter_branches(t, lister=None):
352 by Aaron Bentley
Added branches subcommand
323
    """Iterate through all the branches under a transport"""
354 by Aaron Bentley
Added apache index scraping to the branches command
324
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
325
        try:
326
            branch = bzrdir.open_branch()
327
            if branch.bzrdir is bzrdir:
328
                yield branch
329
        except (NotBranchError, UnsupportedFormatError):
330
            pass
331
354 by Aaron Bentley
Added apache index scraping to the branches command
332
333
def iter_branch_tree(t, lister=None):
334
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
335
        try:
336
            wt = bzrdir.open_workingtree()
337
            yield wt.branch, wt
338
        except NoWorkingTree, UnsupportedFormatError:
339
            try:
340
                branch = bzrdir.open_branch()
341
                if branch.bzrdir is bzrdir:
342
                    yield branch, None
343
            except (NotBranchError, UnsupportedFormatError):
344
                continue
345
356 by Aaron Bentley
Fixed robustness issues in branches command
346
354 by Aaron Bentley
Added apache index scraping to the branches command
347
def iter_bzrdirs(t, lister=None):
348
    if lister is None:
349
        def lister(t):
350
            return t.list_dir('.')
352 by Aaron Bentley
Added branches subcommand
351
    try:
352
        bzrdir = bzrdir_from_transport(t)
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
353
        yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
354
    except (NotBranchError, UnsupportedFormatError, TransportError,
355
            PermissionDenied):
352 by Aaron Bentley
Added branches subcommand
356
        pass
357
    try:
354 by Aaron Bentley
Added apache index scraping to the branches command
358
        for directory in lister(t):
352 by Aaron Bentley
Added branches subcommand
359
            if directory == ".bzr":
360
                continue
356 by Aaron Bentley
Fixed robustness issues in branches command
361
            try:
362
                subt = t.clone(directory)
363
            except UnicodeDecodeError:
364
                continue
357 by Aaron Bentley
Tweakage of the apache scraper
365
            for bzrdir in iter_bzrdirs(subt, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
366
                yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
367
    except (NoSuchFile, PermissionDenied, TransportError):
352 by Aaron Bentley
Added branches subcommand
368
        pass
369
370
    
371
def bzrdir_from_transport(t):
372
    """Open a bzrdir from a transport (not a location)"""
373
    format = BzrDirFormat.find_format(t)
374
    BzrDir._check_supported(format, False)
375
    return format.open(t)
376
377
19 by abentley
librified most of the pull script
378
def run_tests():
16 by abentley
Got is_clean under test, added setters/getters for pull data
379
    import doctest
18 by abentley
Finished implementing bzr-pull
380
    result = doctest.testmod()
19 by abentley
librified most of the pull script
381
    if result[1] > 0:
382
        if result[0] == 0:
383
            print "All tests passed"
384
    else:
385
        print "No tests to run"
386
if __name__ == "__main__":
387
    run_tests()