~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz2bzr

  • Committer: Aaron Bentley
  • Date: 2005-06-14 15:52:58 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050614155258-a6efa8c45a25fd6c
Moved most baz2bzr code to baz_import, added Python 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
 
try:
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
 
    sys.exit(1)
27
 
from pybaz.backends.baz import null_cmd
28
 
import tempfile
29
 
import os
 
20
from baz_import import import_version, UserError
 
21
from progress import rewriting_supported
 
22
import pybaz
 
23
import sys
30
24
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
530
25
 
531
26
def main(args):
532
27
    """Just the main() function for this script.
565
60
 
566
61
    if opts.test:
567
62
        print "Running tests"
568
 
        import doctest
569
 
        nfail, ntests = doctest.testmod(verbose=opts.verbose)
 
63
        import doctest, baz_import
 
64
        nfail, ntests = doctest.testmod(baz_import, verbose=opts.verbose)
570
65
        if nfail > 0:
571
66
            return 1
572
67
        else:
573
68
            return 0
574
69
    if len(args) == 2:
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
 
70
        version,output_dir = args[1]
581
71
            
582
72
    elif len(args) == 1:
583
 
        output_dir = os.path.realpath(args[0])
 
73
        output_dir = args[0]
584
74
        version = None
585
75
    else:
586
76
        print 'Invalid number of arguments, try --help for more info'
587
77
        return 1
 
78
 
 
79
    output_dir = os.path.realpath(output_dir)
 
80
    if version is not None:
 
81
        try:
 
82
            version = pybaz.Version(version)
 
83
        except pybaz.errors.NamespaceError:
 
84
            print "%s is not a valid Arch branch." % version
 
85
            return 1
588
86
        
589
87
    try:
590
88
        fancy = rewriting_supported()
600
98
        print "Aborted."
601
99
        return 1
602
100
 
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)
623
101
        
624
102
 
625
103
if __name__ == '__main__':