~abentley/bzrtools/bzrtools.dev

612 by Aaron Bentley
Update email address
1
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron@aaronbentley.com>
513 by Aaron Bentley
Fix change dectection for dirstate
2
# Copyright (C) 2007 John Arbash Meinel
89 by Aaron Bentley
Added copyright/GPL notices
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
606 by Aaron Bentley
Support branch6 formats in rspush
27
from bzrlib import revision as _mod_revision, trace, urlutils
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
28
import bzrlib.errors
583 by Aaron Bentley
rspush requires standalone trees
29
from bzrlib.errors import (
30
    BzrCommandError,
31
    BzrError,
32
    ConnectionError,
33
    NotBranchError,
34
    NoSuchFile,
35
    NoWorkingTree,
36
    PermissionDenied,
37
    UnsupportedFormatError,
38
    TransportError,
39
    )
352 by Aaron Bentley
Added branches subcommand
40
from bzrlib.bzrdir import BzrDir, BzrDirFormat
563 by Aaron Bentley
Allow importing directly from a URL
41
from bzrlib.transport import get_transport
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
42
43
def temp_tree():
16 by abentley
Got is_clean under test, added setters/getters for pull data
44
    dirname = tempfile.mkdtemp("temp-branch")
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
45
    return BzrDir.create_standalone_workingtree(dirname)
46
47
def rm_tree(tree):
48
    shutil.rmtree(tree.basedir)
49
50
def is_clean(cur_tree):
16 by abentley
Got is_clean under test, added setters/getters for pull data
51
    """
52
    Return true if no files are modifed or unknown
53
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
54
    old_tree = cur_tree.basis_tree()
55
    new_tree = cur_tree
117 by aaron.bentley at utoronto
Excluded non-source files
56
    non_source = []
513 by Aaron Bentley
Fix change dectection for dirstate
57
    new_tree.lock_read()
58
    try:
59
        for path, file_class, kind, file_id, entry in new_tree.list_files():
60
            if file_class in ('?', 'I'):
61
                non_source.append(path)
62
        delta = new_tree.changes_from(old_tree, want_unchanged=False)
63
    finally:
64
        new_tree.unlock()
257.1.3 by Aaron Bentley
Switched to TreeDelta.has_changed
65
    return not delta.has_changed(), non_source
16 by abentley
Got is_clean under test, added setters/getters for pull data
66
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
67
def set_push_data(tree, location):
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
68
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
20 by abentley
added bzr-push command
69
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
70
def get_push_data(tree):
20 by abentley
added bzr-push command
71
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
72
    >>> tree = temp_tree()
73
    >>> get_push_data(tree) is None
20 by abentley
added bzr-push command
74
    True
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
75
    >>> set_push_data(tree, 'http://somewhere')
76
    >>> get_push_data(tree)
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
77
    u'http://somewhere'
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
78
    >>> rm_tree(tree)
20 by abentley
added bzr-push command
79
    """
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
80
    try:
81
        location = tree.branch.control_files.get_utf8('x-push-data').read()
82
    except NoSuchFile:
20 by abentley
added bzr-push command
83
        return None
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
84
    return location.rstrip('\n')
20 by abentley
added bzr-push command
85
19 by abentley
librified most of the pull script
86
"""
87
>>> shell_escape('hello')
88
'\h\e\l\l\o'
89
"""
90
def shell_escape(arg):
91
    return "".join(['\\'+c for c in arg])
92
93
def safe_system(args):
94
    """
95
    >>> real_system = os.system
96
    >>> os.system = sys.stdout.write
97
    >>> safe_system(['a', 'b', 'cd'])
98
    \\a \\b \\c\\d
99
    >>> os.system = real_system
100
    """
101
    arg_str = " ".join([shell_escape(a) for a in args])
102
    return os.system(arg_str)
103
195 by Aaron Bentley
prevented accidental overwrites from push
104
class RsyncUnknownStatus(Exception):
105
    def __init__(self, status):
106
        Exception.__init__(self, "Unknown status: %d" % status)
107
199 by Aaron Bentley
Updated doctests
108
class NoRsync(Exception):
109
    def __init__(self, rsync_name):
110
        Exception.__init__(self, "%s not found." % rsync_name)
111
583 by Aaron Bentley
rspush requires standalone trees
112
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
113
def rsync(source, target, ssh=False, excludes=(), silent=False,
199 by Aaron Bentley
Updated doctests
114
          rsync_name="rsync"):
115
    cmd = [rsync_name, "-av", "--delete"]
20 by abentley
added bzr-push command
116
    if ssh:
117
        cmd.extend(('-e', 'ssh'))
117 by aaron.bentley at utoronto
Excluded non-source files
118
    if len(excludes) > 0:
119
        cmd.extend(('--exclude-from', '-'))
20 by abentley
added bzr-push command
120
    cmd.extend((source, target))
195 by Aaron Bentley
prevented accidental overwrites from push
121
    if silent:
122
        stderr = PIPE
123
        stdout = PIPE
124
    else:
125
        stderr = None
126
        stdout = None
199 by Aaron Bentley
Updated doctests
127
    try:
128
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
129
    except OSError, e:
130
        if e.errno == errno.ENOENT:
131
            raise NoRsync(rsync_name)
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
132
117 by aaron.bentley at utoronto
Excluded non-source files
133
    proc.stdin.write('\n'.join(excludes)+'\n')
134
    proc.stdin.close()
195 by Aaron Bentley
prevented accidental overwrites from push
135
    if silent:
136
        proc.stderr.read()
137
        proc.stderr.close()
138
        proc.stdout.read()
139
        proc.stdout.close()
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
140
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
141
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
142
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
143
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
144
        raise RsyncNoFile(source)
145
    elif proc.returncode != 0:
146
        raise RsyncUnknownStatus(proc.returncode)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
147
    return cmd
117 by aaron.bentley at utoronto
Excluded non-source files
148
195 by Aaron Bentley
prevented accidental overwrites from push
149
150
def rsync_ls(source, ssh=False, silent=True):
151
    cmd = ["rsync"]
152
    if ssh:
153
        cmd.extend(('-e', 'ssh'))
154
    cmd.append(source)
155
    if silent:
156
        stderr = PIPE
157
    else:
158
        stderr = None
159
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
160
    result = proc.stdout.read()
161
    proc.stdout.close()
162
    if silent:
163
        proc.stderr.read()
164
        proc.stderr.close()
165
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
166
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
167
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
168
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
169
        raise RsyncNoFile(source)
170
    elif proc.returncode != 0:
171
        raise RsyncUnknownStatus(proc.returncode)
172
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
173
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
174
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
332 by Aaron Bentley
Added exclusions for x-push and parent in format6 branches
175
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
176
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
177
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
19 by abentley
librified most of the pull script
178
20 by abentley
added bzr-push command
179
195 by Aaron Bentley
prevented accidental overwrites from push
180
def read_revision_history(fname):
181
    return [l.rstrip('\r\n') for l in
182
            codecs.open(fname, 'rb', 'utf-8').readlines()]
183
606 by Aaron Bentley
Support branch6 formats in rspush
184
185
def read_revision_info(path):
186
    """Parse a last_revision file to determine revision_info"""
187
    line = open(path, 'rb').readlines()[0].strip('\n')
188
    revno, revision_id = line.split(' ', 1)
189
    revno = int(revno)
190
    return revno, revision_id
191
192
195 by Aaron Bentley
prevented accidental overwrites from push
193
class RsyncNoFile(Exception):
194
    def __init__(self, path):
195
        Exception.__init__(self, "No such file %s" % path)
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
196
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
197
class RsyncStreamIO(Exception):
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
198
    def __init__(self):
199
        Exception.__init__(self, "Error in rsync protocol data stream.")
195 by Aaron Bentley
prevented accidental overwrites from push
200
583 by Aaron Bentley
rspush requires standalone trees
201
202
class NotStandalone(BzrError):
203
204
    _format = '%(location) is not a standalone tree.'
205
    _internal = False
206
207
    def __init__(self, location):
208
        BzrError.__init__(self, location=location)
209
210
211
def get_revision_history(location, _rsync):
195 by Aaron Bentley
prevented accidental overwrites from push
212
    tempdir = tempfile.mkdtemp('push')
583 by Aaron Bentley
rspush requires standalone trees
213
    my_rsync = _rsync
214
    if my_rsync is None:
215
        my_rsync = rsync
195 by Aaron Bentley
prevented accidental overwrites from push
216
    try:
217
        history_fname = os.path.join(tempdir, 'revision-history')
330 by Aaron Bentley
Got push working with new-format branches
218
        try:
583 by Aaron Bentley
rspush requires standalone trees
219
            cmd = my_rsync(location+'.bzr/revision-history', history_fname,
330 by Aaron Bentley
Got push working with new-format branches
220
                        silent=True)
221
        except RsyncNoFile:
222
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
223
                        silent=True)
195 by Aaron Bentley
prevented accidental overwrites from push
224
        history = read_revision_history(history_fname)
225
    finally:
226
        shutil.rmtree(tempdir)
227
    return history
228
583 by Aaron Bentley
rspush requires standalone trees
229
606 by Aaron Bentley
Support branch6 formats in rspush
230
def get_revision_info(location, _rsync):
231
    """Get the revsision_info for an rsync-able branch"""
232
    tempdir = tempfile.mkdtemp('push')
233
    my_rsync = _rsync
234
    if my_rsync is None:
235
        my_rsync = rsync
236
    try:
237
        info_fname = os.path.join(tempdir, 'last-revision')
238
        cmd = rsync(location+'.bzr/branch/last-revision', info_fname,
239
                    silent=True)
240
        return read_revision_info(info_fname)
241
    finally:
242
        shutil.rmtree(tempdir)
243
244
583 by Aaron Bentley
rspush requires standalone trees
245
def history_subset(location, branch, _rsync=None):
195 by Aaron Bentley
prevented accidental overwrites from push
246
    local_history = branch.revision_history()
606 by Aaron Bentley
Support branch6 formats in rspush
247
    try:
248
        remote_history = get_revision_history(location, _rsync)
249
    except RsyncNoFile:
250
        revno, revision_id = get_revision_info(location, _rsync)
251
        if revision_id == _mod_revision.NULL_REVISION:
252
            return True
253
        return bool(revision_id.decode('utf-8') in local_history)
254
    else:
255
        if len(remote_history) > len(local_history):
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
256
            return False
606 by Aaron Bentley
Support branch6 formats in rspush
257
        for local, remote in zip(remote_history, local_history):
258
            if local != remote:
259
                return False
260
        return True
261
195 by Aaron Bentley
prevented accidental overwrites from push
262
263
def empty_or_absent(location):
264
    try:
265
        files = rsync_ls(location)
266
        return files == ['.']
267
    except RsyncNoFile:
268
        return True
269
583 by Aaron Bentley
rspush requires standalone trees
270
def rspush(tree, location=None, overwrite=False, working_tree=True,
271
    _rsync=None):
584 by Aaron Bentley
Fix rspush lock error when excluding working tree
272
    tree.lock_write()
273
    try:
274
        my_rsync = _rsync
275
        if my_rsync is None:
276
            my_rsync = rsync
277
        if (tree.bzrdir.root_transport.base !=
278
            tree.branch.bzrdir.root_transport.base):
279
            raise NotStandalone(tree.bzrdir.root_transport.base)
280
        if (tree.branch.get_bound_location() is not None):
281
            raise NotStandalone(tree.bzrdir.root_transport.base)
282
        if (tree.branch.repository.is_shared()):
283
            raise NotStandalone(tree.bzrdir.root_transport.base)
284
        push_location = get_push_data(tree)
285
        if location is not None:
286
            if not location.endswith('/'):
287
                location += '/'
288
            push_location = location
289
290
        if push_location is None:
291
            raise BzrCommandError("No rspush location known or specified.")
292
293
        if (push_location.find('::') != -1):
294
            usessh=False
295
        else:
296
            usessh=True
297
298
        if (push_location.find('://') != -1 or
299
            push_location.find(':') == -1):
300
            raise BzrCommandError("Invalid rsync path %r." % push_location)
301
302
        if working_tree:
303
            clean, non_source = is_clean(tree)
304
            if not clean:
593 by Aaron Bentley
Clean up test kipple
305
                raise bzrlib.errors.BzrCommandError(
306
                    'This tree has uncommitted changes or unknown'
307
                    ' (?) files.  Use "bzr status" to list them.')
584 by Aaron Bentley
Fix rspush lock error when excluding working tree
308
                sys.exit(1)
309
            final_exclusions = non_source[:]
310
        else:
311
            wt = tree
312
            final_exclusions = []
313
            for path, status, kind, file_id, entry in wt.list_files():
314
                final_exclusions.append(path)
315
316
        final_exclusions.extend(exclusions)
317
        if not overwrite:
318
            try:
319
                if not history_subset(push_location, tree.branch,
320
                                      _rsync=my_rsync):
321
                    raise bzrlib.errors.BzrCommandError(
322
                        "Local branch is not a newer version of remote"
323
                        " branch.")
324
            except RsyncNoFile:
325
                if not empty_or_absent(push_location):
326
                    raise bzrlib.errors.BzrCommandError(
327
                        "Remote location is not a bzr branch (or empty"
328
                        " directory)")
329
            except RsyncStreamIO:
330
                raise bzrlib.errors.BzrCommandError("Rsync could not use the"
331
                    " specified location.  Please ensure that"
332
                    ' "%s" is of the form "machine:/path".' % push_location)
593 by Aaron Bentley
Clean up test kipple
333
        trace.note("Pushing to %s", push_location)
584 by Aaron Bentley
Fix rspush lock error when excluding working tree
334
        my_rsync(tree.basedir+'/', push_location, ssh=usessh,
606 by Aaron Bentley
Support branch6 formats in rspush
335
                 excludes=final_exclusions)
584 by Aaron Bentley
Fix rspush lock error when excluding working tree
336
337
        set_push_data(tree, push_location)
338
    finally:
339
        tree.unlock()
20 by abentley
added bzr-push command
340
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
341
292 by Aaron Bentley
Introduced branch-history command
342
def short_committer(committer):
343
    new_committer = re.sub('<.*>', '', committer).strip(' ')
344
    if len(new_committer) < 2:
345
        return committer
346
    return new_committer
347
348
354 by Aaron Bentley
Added apache index scraping to the branches command
349
def apache_ls(t):
350
    """Screen-scrape Apache listings"""
355 by Aaron Bentley
Handled more hrefs
351
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
352
        ' <a href="'
571 by Aaron Bentley
Branches works with Apache again
353
    t = t.clone()
354
    t._remote_path = lambda x: t.base
602 by Aaron Bentley
Update branches to new find_branches API
355
    try:
356
        lines = t.get('')
357
    except bzrlib.errors.NoSuchFile:
358
        return
359
    expr = re.compile('<a[^>]*href="([^>]*)\/"[^>]*>', flags=re.I)
354 by Aaron Bentley
Added apache index scraping to the branches command
360
    for line in lines:
355 by Aaron Bentley
Handled more hrefs
361
        match = expr.search(line)
362
        if match is None:
363
            continue
364
        url = match.group(1)
365
        if url.startswith('http://') or url.startswith('/') or '../' in url:
366
            continue
357 by Aaron Bentley
Tweakage of the apache scraper
367
        if '?' in url:
368
            continue
355 by Aaron Bentley
Handled more hrefs
369
        yield url.rstrip('/')
354 by Aaron Bentley
Added apache index scraping to the branches command
370
371
603 by Aaron Bentley
Update branches, multi-pull to new APIs, create trees
372
def list_branches(t):
373
    def is_inside(branch):
374
        return bool(branch.base.startswith(t.base))
375
376
    if t.base.startswith('http://'):
377
        def evaluate(bzrdir):
378
            try:
379
                branch = bzrdir.open_branch()
380
                if is_inside(branch):
381
                    return True, branch
382
                else:
383
                    return True, None
604 by Aaron Bentley
Fix error handling
384
            except NotBranchError:
603 by Aaron Bentley
Update branches, multi-pull to new APIs, create trees
385
                return True, None
386
        return [b for b in BzrDir.find_bzrdirs(t, list_current=apache_ls,
387
                evaluate=evaluate) if b is not None]
388
    elif not t.listable():
389
        raise BzrCommandError("Can't list this type of location.")
390
    return [b for b in BzrDir.find_branches(t) if is_inside(b)]
391
392
393
def evaluate_branch_tree(bzrdir):
394
    try:
395
        tree, branch = bzrdir._get_tree_branch()
396
    except NotBranchError:
397
        return True, None
398
    else:
399
        return True, (branch, tree)
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
400
354 by Aaron Bentley
Added apache index scraping to the branches command
401
402
def iter_branch_tree(t, lister=None):
603 by Aaron Bentley
Update branches, multi-pull to new APIs, create trees
403
    return (x for x in BzrDir.find_bzrdirs(t, evaluate=evaluate_branch_tree,
404
            list_current=lister) if x is not None)
352 by Aaron Bentley
Added branches subcommand
405
406
563 by Aaron Bentley
Allow importing directly from a URL
407
def open_from_url(location):
408
    location = urlutils.normalize_url(location)
409
    dirname, basename = urlutils.split(location)
410
    return get_transport(dirname).get(basename)
411
412
19 by abentley
librified most of the pull script
413
def run_tests():
16 by abentley
Got is_clean under test, added setters/getters for pull data
414
    import doctest
18 by abentley
Finished implementing bzr-pull
415
    result = doctest.testmod()
19 by abentley
librified most of the pull script
416
    if result[1] > 0:
417
        if result[0] == 0:
418
            print "All tests passed"
419
    else:
420
        print "No tests to run"
421
if __name__ == "__main__":
422
    run_tests()