~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz2bzr

  • Committer: Aaron Bentley
  • Date: 2005-06-13 21:30:12 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050613213012-3a3d0db00b50f123
Created push.py plugin

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
#    along with this program; if not, write to the Free Software
18
18
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
 
20
 
import sys
21
 
from errors import NoPyBaz
22
20
try:
23
 
    from baz_import import import_version, UserError
24
 
except NoPyBaz:
25
 
    print >> sys.stderr, "This command requires PyBaz.  Please ensure that it is installed."
 
21
    import pybaz
 
22
    import pybaz.errors
 
23
except ImportError:
 
24
    print "This command requires PyBaz.  Please ensure that it is installed."
 
25
    import sys
26
26
    sys.exit(1)
27
 
 
28
 
import pybaz
 
27
from pybaz.backends.baz import null_cmd
 
28
import tempfile
 
29
import os
29
30
import os.path
 
31
import shutil
 
32
import bzrlib
 
33
from bzrlib.errors import BzrError
 
34
import bzrlib.trace
 
35
import bzrlib.merge
 
36
import sys
 
37
import email.Utils
 
38
from progress import *
 
39
 
 
40
def add_id(files, id=None):
 
41
    """Adds an explicit id to a list of files.
 
42
 
 
43
    :param files: the name of the file to add an id to
 
44
    :type files: list of str
 
45
    :param id: tag one file using the specified id, instead of generating id
 
46
    :type id: str
 
47
    """
 
48
    args = ["add-id"]
 
49
    if id is not None:
 
50
        args.extend(["--id", id])
 
51
    args.extend(files)
 
52
    return null_cmd(args)
 
53
 
 
54
def test_environ():
 
55
    """
 
56
    >>> q = test_environ()
 
57
    >>> os.path.exists(q)
 
58
    True
 
59
    >>> os.path.exists(os.path.join(q, "home", ".arch-params"))
 
60
    True
 
61
    >>> teardown_environ(q)
 
62
    >>> os.path.exists(q)
 
63
    False
 
64
    """
 
65
    tdir = tempfile.mkdtemp(prefix="testdir-")
 
66
    os.environ["HOME"] = os.path.join(tdir, "home")
 
67
    os.mkdir(os.environ["HOME"])
 
68
    arch_dir = os.path.join(tdir, "archive_dir")
 
69
    pybaz.make_archive("test@example.com", arch_dir)
 
70
    work_dir = os.path.join(tdir, "work_dir")
 
71
    os.mkdir(work_dir)
 
72
    os.chdir(work_dir)
 
73
    pybaz.init_tree(work_dir, "test@example.com/test--test--0")
 
74
    lib_dir = os.path.join(tdir, "lib_dir")
 
75
    os.mkdir(lib_dir)
 
76
    pybaz.register_revision_library(lib_dir)
 
77
    pybaz.set_my_id("Test User<test@example.org>")
 
78
    return tdir
 
79
 
 
80
def add_file(path, text, id):
 
81
    """
 
82
    >>> q = test_environ()
 
83
    >>> add_file("path with space", "text", "lalala")
 
84
    >>> tree = pybaz.tree_root(".")
 
85
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
 
86
    >>> ("x_lalala", "path with space") in inv
 
87
    True
 
88
    >>> teardown_environ(q)
 
89
    """
 
90
    file(path, "wb").write(text)
 
91
    add_id([path], id)
 
92
 
 
93
 
 
94
def add_dir(path, id):
 
95
    """
 
96
    >>> q = test_environ()
 
97
    >>> add_dir("path with\(sp) space", "lalala")
 
98
    >>> tree = pybaz.tree_root(".")
 
99
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
 
100
    >>> ("x_lalala", "path with\(sp) space") in inv
 
101
    True
 
102
    >>> teardown_environ(q)
 
103
    """
 
104
    os.mkdir(path)
 
105
    add_id([path], id)
 
106
 
 
107
def teardown_environ(tdir):
 
108
    os.chdir("/")
 
109
    shutil.rmtree(tdir)
 
110
 
 
111
def timport(tree, summary):
 
112
    msg = tree.log_message()
 
113
    msg["summary"] = summary
 
114
    tree.import_(msg)
 
115
 
 
116
def commit(tree, summary):
 
117
    """
 
118
    >>> q = test_environ()
 
119
    >>> tree = pybaz.tree_root(".")
 
120
    >>> timport(tree, "import")
 
121
    >>> commit(tree, "commit")
 
122
    >>> logs = [str(l.revision) for l in tree.iter_logs()]
 
123
    >>> len(logs)
 
124
    2
 
125
    >>> logs[0]
 
126
    'test@example.com/test--test--0--base-0'
 
127
    >>> logs[1]
 
128
    'test@example.com/test--test--0--patch-1'
 
129
    >>> teardown_environ(q)
 
130
    """
 
131
    msg = tree.log_message()
 
132
    msg["summary"] = summary
 
133
    tree.commit(msg)
 
134
 
 
135
def commit_test_revisions():
 
136
    """
 
137
    >>> q = test_environ()
 
138
    >>> commit_test_revisions()
 
139
    >>> a = pybaz.Archive("test@example.com")
 
140
    >>> revisions = list(a.iter_revisions("test--test--0"))
 
141
    >>> len(revisions)
 
142
    3
 
143
    >>> str(revisions[2])
 
144
    'test@example.com/test--test--0--base-0'
 
145
    >>> str(revisions[1])
 
146
    'test@example.com/test--test--0--patch-1'
 
147
    >>> str(revisions[0])
 
148
    'test@example.com/test--test--0--patch-2'
 
149
    >>> teardown_environ(q)
 
150
    """
 
151
    tree = pybaz.tree_root(".")
 
152
    add_file("mainfile", "void main(void){}", "mainfile by aaron")
 
153
    timport(tree, "Created mainfile")
 
154
    file("mainfile", "wb").write("or something like that")
 
155
    commit(tree, "altered mainfile")
 
156
    add_file("ofile", "this is another file", "ofile by aaron")
 
157
    commit(tree, "altered mainfile")
 
158
 
 
159
 
 
160
def commit_more_test_revisions():
 
161
    """
 
162
    >>> q = test_environ()
 
163
    >>> commit_test_revisions()
 
164
    >>> commit_more_test_revisions()
 
165
    >>> a = pybaz.Archive("test@example.com")
 
166
    >>> revisions = list(a.iter_revisions("test--test--0"))
 
167
    >>> len(revisions)
 
168
    4
 
169
    >>> str(revisions[0])
 
170
    'test@example.com/test--test--0--patch-3'
 
171
    >>> teardown_environ(q)
 
172
    """
 
173
    tree = pybaz.tree_root(".")
 
174
    add_file("trainfile", "void train(void){}", "trainfile by aaron")
 
175
    commit(tree, "altered trainfile")
 
176
 
 
177
class NoSuchVersion(Exception):
 
178
    def __init__(self, version):
 
179
        Exception.__init__(self, "The version %s does not exist." % version)
 
180
        self.version = version
 
181
 
 
182
def version_ancestry(version):
 
183
    """
 
184
    >>> q = test_environ()
 
185
    >>> commit_test_revisions()
 
186
    >>> version = pybaz.Version("test@example.com/test--test--0")
 
187
    >>> ancestors = version_ancestry(version)
 
188
    >>> str(ancestors[0])
 
189
    'test@example.com/test--test--0--base-0'
 
190
    >>> str(ancestors[1])
 
191
    'test@example.com/test--test--0--patch-1'
 
192
    >>> version = pybaz.Version("test@example.com/test--test--0.5")
 
193
    >>> ancestors = version_ancestry(version)
 
194
    Traceback (most recent call last):
 
195
    NoSuchVersion: The version test@example.com/test--test--0.5 does not exist.
 
196
    >>> teardown_environ(q)
 
197
    """
 
198
    try:
 
199
        revision = version.iter_revisions(reverse=True).next()
 
200
    except:
 
201
        if not version.exists():
 
202
            raise NoSuchVersion(version)
 
203
        else:
 
204
            raise
 
205
    ancestors = list(revision.iter_ancestors(metoo=True))
 
206
    ancestors.reverse()
 
207
    return ancestors
 
208
 
 
209
def get_last_revision(branch):
 
210
    last_patch = branch.last_patch()
 
211
    try:
 
212
        return arch_revision(last_patch)
 
213
    except NotArchRevision:
 
214
        raise UserError(
 
215
            "Directory \"%s\" already exists, and the last revision is not"
 
216
            " an Arch revision (%s)" % (output_dir, last_patch))
 
217
 
 
218
 
 
219
def get_remaining_revisions(output_dir, version):
 
220
    last_patch = None
 
221
    old_revno = None
 
222
    if os.path.exists(output_dir):
 
223
        # We are starting from an existing directory, figure out what
 
224
        # the current version is
 
225
        branch = find_branch(output_dir)
 
226
        last_patch = get_last_revision(branch)
 
227
        if version is None:
 
228
            version = last_patch.version
 
229
    elif version is None:
 
230
        raise UserError("No version specified, and directory does not exist.")
 
231
 
 
232
    try:
 
233
        ancestors = version_ancestry(version)
 
234
    except NoSuchVersion, e:
 
235
        raise UserError(e)
 
236
 
 
237
    if last_patch:
 
238
        for i in range(len(ancestors)):
 
239
            if ancestors[i] == last_patch:
 
240
                break
 
241
        else:
 
242
            raise UserError("Directory \"%s\" already exists, and the last "
 
243
                "revision (%s) is not in the ancestry of %s" % 
 
244
                (output_dir, last_patch, version))
 
245
        # Strip off all of the ancestors which are already present
 
246
        # And get a directory starting with the latest ancestor
 
247
        latest_ancestor = ancestors[i]
 
248
        old_revno = find_branch(output_dir).revno()
 
249
        ancestors = ancestors[i+1:]
 
250
    return ancestors, old_revno
 
251
 
 
252
def import_version(output_dir, version, fancy=True, fast=False, verbose=False, 
 
253
                   dry_run=False, max_count=None, skip_symlinks=False):
 
254
    """
 
255
    >>> q = test_environ()
 
256
    >>> result_path = os.path.join(q, "result")
 
257
    >>> commit_test_revisions()
 
258
    >>> version = pybaz.Version("test@example.com/test--test--0.1")
 
259
    >>> import_version('/', version, fancy=False, dry_run=True)
 
260
    Traceback (most recent call last):
 
261
    UserError: / exists, but is not a bzr branch.
 
262
    >>> import_version(result_path, version, fancy=False, dry_run=True)
 
263
    Traceback (most recent call last):
 
264
    UserError: The version test@example.com/test--test--0.1 does not exist.
 
265
    >>> version = pybaz.Version("test@example.com/test--test--0")
 
266
    >>> import_version(result_path, version, fancy=False, dry_run=True)
 
267
    not fancy
 
268
    ....
 
269
    Dry run, not modifying output_dir
 
270
    Cleaning up
 
271
    >>> import_version(result_path, version, fancy=False)
 
272
    not fancy
 
273
    ....
 
274
    Cleaning up
 
275
    Import complete.
 
276
    >>> import_version(result_path, version, fancy=False)
 
277
    Tree is up-to-date with test@example.com/test--test--0--patch-2
 
278
    >>> commit_more_test_revisions()
 
279
    >>> import_version(result_path, version, fancy=False)
 
280
    not fancy
 
281
    ..
 
282
    Cleaning up
 
283
    Import complete.
 
284
    >>> teardown_environ(q)
 
285
    """
 
286
    try:
 
287
        ancestors, old_revno = get_remaining_revisions(output_dir, version)
 
288
    except NotInABranch, e:
 
289
        raise UserError("%s exists, but is not a bzr branch." % e.path)
 
290
    if len(ancestors) == 0:
 
291
        last_revision = get_last_revision(find_branch(output_dir))
 
292
        print 'Tree is up-to-date with %s' % last_revision
 
293
        return
 
294
 
 
295
    progress_bar = ProgressBar()
 
296
    tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
 
297
                               dir=os.path.dirname(output_dir))
 
298
    try:
 
299
        if not fancy:
 
300
            print "not fancy"
 
301
        try:
 
302
            for result in iter_import_version(output_dir, ancestors, tempdir,
 
303
                    fast=fast, verbose=verbose, dry_run=dry_run, 
 
304
                    max_count=max_count, skip_symlinks=skip_symlinks):
 
305
                if fancy:
 
306
                    progress_bar(result)
 
307
                else:
 
308
                    sys.stdout.write('.')
 
309
        finally:
 
310
            if fancy:
 
311
                clear_progress_bar()
 
312
            else:
 
313
                sys.stdout.write('\n')
 
314
 
 
315
        if dry_run:
 
316
            print 'Dry run, not modifying output_dir'
 
317
            return
 
318
        if os.path.exists(output_dir):
 
319
            # Move the bzr control directory back, and update the working tree
 
320
            tmp_bzr_dir = os.path.join(tempdir, '.bzr')
 
321
            
 
322
            bzr_dir = os.path.join(output_dir, '.bzr')
 
323
            new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
324
 
 
325
            os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
 
326
            os.rename(new_bzr_dir, bzr_dir)
 
327
            try:
 
328
                bzrlib.merge.merge((output_dir, -1), (output_dir, old_revno), 
 
329
                                   check_clean=False, this_dir=output_dir, 
 
330
                                   ignore_zero=True)
 
331
            except:
 
332
                # If something failed, move back the original bzr directory
 
333
                os.rename(bzr_dir, new_bzr_dir)
 
334
                os.rename(tmp_bzr_dir, bzr_dir)
 
335
                raise
 
336
        else:
 
337
            revdir = os.path.join(tempdir, "rd")
 
338
            os.rename(revdir, output_dir)
 
339
 
 
340
    finally:
 
341
        print 'Cleaning up'
 
342
        shutil.rmtree(tempdir)
 
343
    print "Import complete."
 
344
            
 
345
class UserError(Exception):
 
346
    def __init__(self, message):
 
347
        """Exception to throw when a user makes an impossible request
 
348
        :param message: The message to emit when printing this exception
 
349
        :type message: string
 
350
        """
 
351
        Exception.__init__(self, message)
 
352
 
 
353
def revision_id(arch_revision):
 
354
    """
 
355
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
 
356
    designates a revision imported with an experimental algorithm.  A number
 
357
    would indicate a particular standardized version.
 
358
 
 
359
    :param arch_revision: The Arch revision to generate an ID for.
 
360
 
 
361
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
 
362
    'Arch-x:you@example.com%cat--br--0--base-0'
 
363
    """
 
364
    return "Arch-x:%s" % str(arch_revision).replace('/', '%')
 
365
 
 
366
class NotArchRevision(Exception):
 
367
    def __init__(self, revision_id):
 
368
        msg = "The revision id %s does not look like it came from Arch."\
 
369
            % revision_id
 
370
        Exception.__init__(self, msg)
 
371
 
 
372
def arch_revision(revision_id):
 
373
    """
 
374
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0"))
 
375
    Traceback (most recent call last):
 
376
    NotArchRevision: The revision id Arch-x:jrandom@example.com%test--test--0 does not look like it came from Arch.
 
377
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0--base-5"))
 
378
    Traceback (most recent call last):
 
379
    NotArchRevision: The revision id Arch-x:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
 
380
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0--patch-5"))
 
381
    'jrandom@example.com/test--test--0--patch-5'
 
382
    """
 
383
    if revision_id is None:
 
384
        return None
 
385
    if revision_id[:7] != 'Arch-x:':
 
386
        raise NotArchRevision(revision_id)
 
387
    else:
 
388
        try:
 
389
            return pybaz.Revision(revision_id[7:].replace('%', '/'))
 
390
        except pybaz.errors.NamespaceError, e:
 
391
            raise NotArchRevision(revision_id)
 
392
            
 
393
def iter_import_version(output_dir, ancestors, tempdir, fast=False,
 
394
                        verbose=False, dry_run=False, max_count=None,
 
395
                        skip_symlinks=False):
 
396
    revdir = None
 
397
 
 
398
    # Uncomment this for testing, it basically just has baz2bzr only update
 
399
    # 5 patches at a time
 
400
    if max_count:
 
401
        ancestors = ancestors[:max_count]
 
402
 
 
403
    # Not sure if I want this output. basically it tells you ahead of time
 
404
    # what it is going to do, but then later it tells you as it is doing it.
 
405
    # what probably would be best would be to collapse it into ranges, so that
 
406
    # this gives the simple view, and then later it gives the blow by blow.
 
407
    #if verbose:
 
408
    #    print 'Adding the following revisions:'
 
409
    #    for a in ancestors:
 
410
    #        print '\t%s' % a
 
411
 
 
412
    previous_version=None
 
413
 
 
414
    for i in range(len(ancestors)):
 
415
        revision = ancestors[i]
 
416
        if verbose:
 
417
            version = str(revision.version)
 
418
            if version != previous_version:
 
419
                clear_progress_bar()
 
420
                print '\rOn version: %s' % version
 
421
            yield Progress(str(revision.patchlevel), i, len(ancestors))
 
422
            previous_version = version
 
423
        else:
 
424
            yield Progress("revisions", i, len(ancestors))
 
425
        if revdir is None:
 
426
            revdir = os.path.join(tempdir, "rd")
 
427
            baz_inv, log = get_revision(revdir, revision, 
 
428
                                        skip_symlinks=skip_symlinks)
 
429
            if os.path.exists(output_dir):
 
430
                bzr_dir = os.path.join(output_dir, '.bzr')
 
431
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
432
                # This would be much faster with a simple os.rename(), but if
 
433
                # we fail, we have corrupted the original .bzr directory.  Is
 
434
                # that a big problem, as we can just back out the last
 
435
                # revisions in .bzr/revision_history I don't really know
 
436
                shutil.copytree(bzr_dir, new_bzr_dir)
 
437
                # Now revdir should have a tree with the latest .bzr, and the
 
438
                # next revision of the baz tree
 
439
                branch = find_branch(revdir)
 
440
            else:
 
441
                branch = bzrlib.Branch(revdir, init=True)
 
442
        else:
 
443
            old = os.path.join(revdir, ".bzr")
 
444
            new = os.path.join(tempdir, ".bzr")
 
445
            os.rename(old, new)
 
446
            baz_inv, log = apply_revision(revdir, revision, 
 
447
                                          skip_symlinks=skip_symlinks)
 
448
            os.rename(new, old)
 
449
            branch = find_branch(revdir)
 
450
        timestamp = email.Utils.mktime_tz(log.date + (0,))
 
451
        rev_id = revision_id(revision)
 
452
        branch.lock_write()
 
453
        try:
 
454
            branch.set_inventory(baz_inv)
 
455
            bzrlib.trace.silent = True
 
456
            branch.commit(log.summary, verbose=False, committer=log.creator,
 
457
                          timestamp=timestamp, timezone=0, rev_id=rev_id)
 
458
        finally:
 
459
            bzrlib.trace.silent = False   
 
460
            branch.unlock()
 
461
    yield Progress("revisions", len(ancestors), len(ancestors))
 
462
    unlink_unversioned(branch, revdir)
 
463
 
 
464
def unlink_unversioned(branch, revdir):
 
465
    for unversioned in branch.working_tree().extras():
 
466
        path = os.path.join(revdir, unversioned)
 
467
        if os.path.isdir(path):
 
468
            shutil.rmtree(path)
 
469
        else:
 
470
            os.unlink(path)
 
471
 
 
472
def get_log(tree, revision):
 
473
    log = tree.iter_logs(version=revision.version, reverse=True).next()
 
474
    assert log.revision == revision
 
475
    return log
 
476
 
 
477
def get_revision(revdir, revision, skip_symlinks=False):
 
478
    revision.get(revdir)
 
479
    tree = pybaz.tree_root(revdir)
 
480
    log = get_log(tree, revision)
 
481
    try:
 
482
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log 
 
483
    except BadFileKind, e:
 
484
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
485
 
 
486
 
 
487
def apply_revision(revdir, revision, skip_symlinks=False):
 
488
    tree = pybaz.tree_root(revdir)
 
489
    revision.apply(tree)
 
490
    log = get_log(tree, revision)
 
491
    try:
 
492
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log
 
493
    except BadFileKind, e:
 
494
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
495
 
 
496
 
 
497
 
 
498
 
 
499
class BadFileKind(Exception):
 
500
    """The file kind is not permitted in bzr inventories"""
 
501
    def __init__(self, tree_root, path, kind):
 
502
        self.tree_root = tree_root
 
503
        self.path = path
 
504
        self.kind = kind
 
505
        Exception.__init__(self, "File %s is of forbidden type %s" %
 
506
                           (os.path.join(tree_root, path), kind))
 
507
 
 
508
def bzr_inventory_data(tree, skip_symlinks=False):
 
509
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
 
510
    inv_map = {}
 
511
    for file_id, path in inv_iter:
 
512
        inv_map[path] = file_id 
 
513
 
 
514
    bzr_inv = []
 
515
    for path, file_id in inv_map.iteritems():
 
516
        full_path = os.path.join(tree, path)
 
517
        kind = bzrlib.osutils.file_kind(full_path)
 
518
        if skip_symlinks and kind == "symlink":
 
519
            continue
 
520
        if kind not in ("file", "directory"):
 
521
            raise BadFileKind(tree, path, kind)
 
522
        parent_dir = os.path.dirname(path)
 
523
        if parent_dir != "":
 
524
            parent_id = inv_map[parent_dir]
 
525
        else:
 
526
            parent_id = bzrlib.inventory.ROOT_ID
 
527
        bzr_inv.append((path, file_id, parent_id, kind))
 
528
    bzr_inv.sort()
 
529
    return bzr_inv
30
530
 
31
531
def main(args):
32
532
    """Just the main() function for this script.
65
565
 
66
566
    if opts.test:
67
567
        print "Running tests"
68
 
        import doctest, baz_import
69
 
        nfail, ntests = doctest.testmod(baz_import, verbose=opts.verbose)
 
568
        import doctest
 
569
        nfail, ntests = doctest.testmod(verbose=opts.verbose)
70
570
        if nfail > 0:
71
571
            return 1
72
572
        else:
73
573
            return 0
74
574
    if len(args) == 2:
75
 
        version,output_dir = args
 
575
        output_dir = os.path.realpath(args[1])
 
576
        try:
 
577
            version = pybaz.Version(args[0])
 
578
        except pybaz.errors.NamespaceError:
 
579
            print "%s is not a valid Arch branch." % args[0]
 
580
            return 1
76
581
            
77
582
    elif len(args) == 1:
78
 
        output_dir = args[0]
 
583
        output_dir = os.path.realpath(args[0])
79
584
        version = None
80
585
    else:
81
586
        print 'Invalid number of arguments, try --help for more info'
82
587
        return 1
83
 
 
84
 
    output_dir = os.path.realpath(output_dir)
85
 
    if version is not None:
86
 
        try:
87
 
            version = pybaz.Version(version)
88
 
        except pybaz.errors.NamespaceError:
89
 
            print "%s is not a valid Arch branch." % version
90
 
            return 1
91
588
        
92
589
    try:
 
590
        fancy = rewriting_supported()
93
591
        import_version(output_dir, version,
94
592
            verbose=opts.verbose, fast=opts.fast,
95
593
            dry_run=opts.dry_run, max_count=opts.max_count,
96
 
            skip_symlinks=opts.skip_symlinks)
 
594
            skip_symlinks=opts.skip_symlinks, fancy=fancy)
97
595
        return 0
98
596
    except UserError, e:
99
597
        print e
102
600
        print "Aborted."
103
601
        return 1
104
602
 
 
603
class NotInABranch(Exception):
 
604
    def __init__(self, path):
 
605
        Exception.__init__(self, "%s is not in a branch." % path)
 
606
        self.path = path
 
607
 
 
608
 
 
609
def find_branch(path):
 
610
    """
 
611
    >>> find_branch('/')
 
612
    Traceback (most recent call last):
 
613
    NotInABranch: / is not in a branch.
 
614
    >>> sb = bzrlib.ScratchBranch()
 
615
    >>> isinstance(find_branch(sb.base), bzrlib.Branch)
 
616
    True
 
617
    """
 
618
    try:
 
619
        return bzrlib.Branch(path)
 
620
    except BzrError, e:
 
621
        if e.args[0].endswith("' is not in a branch"):
 
622
            raise NotInABranch(path)
105
623
        
106
624
 
107
625
if __name__ == '__main__':