~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz_import.py

  • Committer: Aaron Bentley
  • Date: 2006-03-01 13:42:15 UTC
  • mfrom: (319 bzrtools)
  • mto: (147.4.30 trunk)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: abentley@panoramicfeedback.com-20060301134215-f343b0751787dfb9
Merge push fix

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 by Aaron Bentley
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import errno
 
18
 
 
19
from bzrlib.errors import BzrError
 
20
from bzrlib.errors import NotBranchError, BzrCommandError, NoSuchRevision
 
21
from bzrlib.branch import Branch
 
22
from bzrlib.commit import Commit, NullCommitReporter
 
23
from bzrlib.commands import Command
 
24
from bzrlib.option import _global_option
 
25
from bzrlib.workingtree import WorkingTree
 
26
from errors import NoPyBaz
 
27
try:
 
28
    import pybaz
 
29
    import pybaz.errors
 
30
    from pybaz import NameParser as NameParser
 
31
    from pybaz.backends.baz import null_cmd
 
32
except ImportError:
 
33
    raise NoPyBaz
 
34
from fai import iter_new_merges, direct_merges
 
35
import tempfile
 
36
import os
 
37
import os.path
 
38
import shutil
 
39
import bzrlib
 
40
import bzrlib.builtins
 
41
import bzrlib.trace
 
42
import bzrlib.merge
 
43
import bzrlib.inventory
 
44
import bzrlib.osutils
 
45
import sys
 
46
import email.Utils
 
47
from progress import *
 
48
 
 
49
class ImportCommitReporter(NullCommitReporter):
 
50
    def __init__(self, pb):
 
51
        self.pb = pb
 
52
 
 
53
    def escaped(self, escape_count, message):
 
54
        self.pb.clear()
 
55
        bzrlib.trace.warning("replaced %d control characters in message" %
 
56
                             escape_count)
 
57
 
 
58
def add_id(files, id=None):
 
59
    """Adds an explicit id to a list of files.
 
60
 
 
61
    :param files: the name of the file to add an id to
 
62
    :type files: list of str
 
63
    :param id: tag one file using the specified id, instead of generating id
 
64
    :type id: str
 
65
    """
 
66
    args = ["add-id"]
 
67
    if id is not None:
 
68
        args.extend(["--id", id])
 
69
    args.extend(files)
 
70
    return null_cmd(args)
 
71
 
 
72
saved_dir = None
 
73
 
 
74
def test_environ():
 
75
    """
 
76
    >>> q = test_environ()
 
77
    >>> os.path.exists(q)
 
78
    True
 
79
    >>> os.path.exists(os.path.join(q, "home", ".arch-params"))
 
80
    True
 
81
    >>> teardown_environ(q)
 
82
    >>> os.path.exists(q)
 
83
    False
 
84
    """
 
85
    global saved_dir
 
86
    saved_dir = os.getcwdu()
 
87
    tdir = tempfile.mkdtemp(prefix="testdir-")
 
88
    os.environ["HOME"] = os.path.join(tdir, "home")
 
89
    os.mkdir(os.environ["HOME"])
 
90
    arch_dir = os.path.join(tdir, "archive_dir")
 
91
    pybaz.make_archive("test@example.com", arch_dir)
 
92
    work_dir = os.path.join(tdir, "work_dir")
 
93
    os.mkdir(work_dir)
 
94
    os.chdir(work_dir)
 
95
    pybaz.init_tree(work_dir, "test@example.com/test--test--0")
 
96
    lib_dir = os.path.join(tdir, "lib_dir")
 
97
    os.mkdir(lib_dir)
 
98
    pybaz.register_revision_library(lib_dir)
 
99
    pybaz.set_my_id("Test User<test@example.org>")
 
100
    return tdir
 
101
 
 
102
def add_file(path, text, id):
 
103
    """
 
104
    >>> q = test_environ()
 
105
    >>> add_file("path with space", "text", "lalala")
 
106
    >>> tree = pybaz.tree_root(".")
 
107
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
 
108
    >>> ("x_lalala", "path with space") in inv
 
109
    True
 
110
    >>> teardown_environ(q)
 
111
    """
 
112
    file(path, "wb").write(text)
 
113
    add_id([path], id)
 
114
 
 
115
 
 
116
def add_dir(path, id):
 
117
    """
 
118
    >>> q = test_environ()
 
119
    >>> add_dir("path with\(sp) space", "lalala")
 
120
    >>> tree = pybaz.tree_root(".")
 
121
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
 
122
    >>> ("x_lalala", "path with\(sp) space") in inv
 
123
    True
 
124
    >>> teardown_environ(q)
 
125
    """
 
126
    os.mkdir(path)
 
127
    add_id([path], id)
 
128
 
 
129
def teardown_environ(tdir):
 
130
    os.chdir(saved_dir)
 
131
    shutil.rmtree(tdir)
 
132
 
 
133
def timport(tree, summary):
 
134
    msg = tree.log_message()
 
135
    msg["summary"] = summary
 
136
    tree.import_(msg)
 
137
 
 
138
def commit(tree, summary):
 
139
    """
 
140
    >>> q = test_environ()
 
141
    >>> tree = pybaz.tree_root(".")
 
142
    >>> timport(tree, "import")
 
143
    >>> commit(tree, "commit")
 
144
    >>> logs = [str(l.revision) for l in tree.iter_logs()]
 
145
    >>> len(logs)
 
146
    2
 
147
    >>> logs[0]
 
148
    'test@example.com/test--test--0--base-0'
 
149
    >>> logs[1]
 
150
    'test@example.com/test--test--0--patch-1'
 
151
    >>> teardown_environ(q)
 
152
    """
 
153
    msg = tree.log_message()
 
154
    msg["summary"] = summary
 
155
    tree.commit(msg)
 
156
 
 
157
def commit_test_revisions():
 
158
    """
 
159
    >>> q = test_environ()
 
160
    >>> commit_test_revisions()
 
161
    >>> a = pybaz.Archive("test@example.com")
 
162
    >>> revisions = list(a.iter_revisions("test--test--0"))
 
163
    >>> len(revisions)
 
164
    3
 
165
    >>> str(revisions[2])
 
166
    'test@example.com/test--test--0--base-0'
 
167
    >>> str(revisions[1])
 
168
    'test@example.com/test--test--0--patch-1'
 
169
    >>> str(revisions[0])
 
170
    'test@example.com/test--test--0--patch-2'
 
171
    >>> teardown_environ(q)
 
172
    """
 
173
    tree = pybaz.tree_root(".")
 
174
    add_file("mainfile", "void main(void){}", "mainfile by aaron")
 
175
    timport(tree, "Created mainfile")
 
176
    file("mainfile", "wb").write("or something like that")
 
177
    commit(tree, "altered mainfile")
 
178
    add_file("ofile", "this is another file", "ofile by aaron")
 
179
    commit(tree, "altered mainfile")
 
180
 
 
181
 
 
182
def commit_more_test_revisions():
 
183
    """
 
184
    >>> q = test_environ()
 
185
    >>> commit_test_revisions()
 
186
    >>> commit_more_test_revisions()
 
187
    >>> a = pybaz.Archive("test@example.com")
 
188
    >>> revisions = list(a.iter_revisions("test--test--0"))
 
189
    >>> len(revisions)
 
190
    4
 
191
    >>> str(revisions[0])
 
192
    'test@example.com/test--test--0--patch-3'
 
193
    >>> teardown_environ(q)
 
194
    """
 
195
    tree = pybaz.tree_root(".")
 
196
    add_file("trainfile", "void train(void){}", "trainfile by aaron")
 
197
    commit(tree, "altered trainfile")
 
198
 
 
199
class NoSuchVersion(Exception):
 
200
    def __init__(self, version):
 
201
        Exception.__init__(self, "The version %s does not exist." % version)
 
202
        self.version = version
 
203
 
 
204
def version_ancestry(version):
 
205
    """
 
206
    >>> q = test_environ()
 
207
    >>> commit_test_revisions()
 
208
    >>> version = pybaz.Version("test@example.com/test--test--0")
 
209
    >>> ancestors = version_ancestry(version)
 
210
    >>> str(ancestors[0])
 
211
    'test@example.com/test--test--0--base-0'
 
212
    >>> str(ancestors[1])
 
213
    'test@example.com/test--test--0--patch-1'
 
214
    >>> version = pybaz.Version("test@example.com/test--test--0.5")
 
215
    >>> ancestors = version_ancestry(version)
 
216
    Traceback (most recent call last):
 
217
    NoSuchVersion: The version test@example.com/test--test--0.5 does not exist.
 
218
    >>> teardown_environ(q)
 
219
    """
 
220
    try:
 
221
        revision = version.iter_revisions(reverse=True).next()
 
222
    except StopIteration:
 
223
        return ()
 
224
    except:
 
225
        print version
 
226
        if not version.exists():
 
227
            raise NoSuchVersion(version)
 
228
        else:
 
229
            raise
 
230
    ancestors = list(revision.iter_ancestors(metoo=True))
 
231
    ancestors.reverse()
 
232
    return ancestors
 
233
 
 
234
def get_last_revision(branch):
 
235
    last_patch = branch.last_revision()
 
236
    try:
 
237
        return arch_revision(last_patch)
 
238
    except NotArchRevision:
 
239
        raise UserError(
 
240
            "Directory \"%s\" already exists, and the last revision is not"
 
241
            " an Arch revision (%s)" % (branch.base, last_patch))
 
242
 
 
243
def do_branch(br_from, to_location, revision_id):
 
244
    """Derived from branch in builtins."""
 
245
    br_from.lock_read()
 
246
    try:
 
247
        try:
 
248
            os.mkdir(to_location)
 
249
        except OSError, e:
 
250
            if e.errno == errno.EEXIST:
 
251
                raise UserError('Target directory "%s" already'
 
252
                                      ' exists.' % to_location)
 
253
            if e.errno == errno.ENOENT:
 
254
                raise UserError('Parent of "%s" does not exist.' %
 
255
                                      to_location)
 
256
            else:
 
257
                raise
 
258
        try:
 
259
            br_from.bzrdir.clone(to_location, revision_id)
 
260
#            copy_branch(br_from, to_location, revision_id, None)
 
261
        except NoSuchRevision:
 
262
            rmtree(to_location)
 
263
            msg = "The branch %s has no revision %s." % (from_location, revision_id)
 
264
            raise UserError(msg)
 
265
    finally:
 
266
        br_from.unlock()
 
267
 
 
268
def get_remaining_revisions(output_dir, version, reuse_history_from=[]):
 
269
    last_patch = None
 
270
    old_revno = None
 
271
    output_exists = os.path.exists(output_dir)
 
272
    if output_exists:
 
273
        # We are starting from an existing directory, figure out what
 
274
        # the current version is
 
275
        branch = Branch.open(output_dir)
 
276
        last_patch = get_last_revision(branch)
 
277
        if last_patch is None:
 
278
            raise NotPreviousImport(branch.base)
 
279
        if version is None:
 
280
            version = last_patch.version
 
281
    elif version is None:
 
282
        raise UserError("No version specified, and directory does not exist.")
 
283
 
 
284
    try:
 
285
        ancestors = version_ancestry(version)
 
286
        if not output_exists and reuse_history_from != []:
 
287
            for ancestor in reversed(ancestors):
 
288
                if last_patch is not None:
 
289
                    # found something to copy
 
290
                    break
 
291
                # try to grab a copy of ancestor
 
292
                # note that is not optimised: we could look for namespace
 
293
                # transitions and only look for the past after the 
 
294
                # transition.
 
295
                for history_root in reuse_history_from:
 
296
                    possible_source = os.path.join(history_root,
 
297
                        map_namespace(ancestor.version))
 
298
                    try:
 
299
                        source = Branch.open(possible_source)
 
300
                        rev_id = revision_id(ancestor)
 
301
                        if rev_id in source.revision_history():
 
302
                            do_branch(source, output_dir, rev_id)
 
303
                            last_patch = ancestor
 
304
                            break
 
305
                    except NotBranchError:
 
306
                        pass
 
307
    except NoSuchVersion, e:
 
308
        raise UserError(str(e))
 
309
 
 
310
    if last_patch:
 
311
        for i in range(len(ancestors)):
 
312
            if ancestors[i] == last_patch:
 
313
                break
 
314
        else:
 
315
            raise UserError("Directory \"%s\" already exists, and the last "
 
316
                "revision (%s) is not in the ancestry of %s" % 
 
317
                (output_dir, last_patch, version))
 
318
        # Strip off all of the ancestors which are already present
 
319
        # And get a directory starting with the latest ancestor
 
320
        latest_ancestor = ancestors[i]
 
321
        old_revno = Branch.open(output_dir).revno()
 
322
        ancestors = ancestors[i+1:]
 
323
    return ancestors, old_revno
 
324
 
 
325
 
 
326
###class Importer(object):
 
327
###    """An importer.
 
328
###    
 
329
###    Currently this is used as a parameter object, though more behaviour is
 
330
###    possible later.
 
331
###    """
 
332
###
 
333
###    def __init__(self, output_dir, version, printer, fancy=True, fast=False,
 
334
###                 verbose=False, dry_run=False, max_count=None, 
 
335
###                   reuse_history_from=[]):
 
336
###        self.output_dir = output_dir
 
337
###        self.version = version
 
338
###        self.
 
339
 
 
340
 
 
341
def import_version(output_dir, version, printer, fancy=True, fast=False,
 
342
                   verbose=False, dry_run=False, max_count=None,
 
343
                   reuse_history_from=[]):
 
344
    """
 
345
    >>> q = test_environ()
 
346
    >>> result_path = os.path.join(q, "result")
 
347
    >>> commit_test_revisions()
 
348
    >>> version = pybaz.Version("test@example.com/test--test--0.1")
 
349
    >>> def printer(message): print message
 
350
    >>> import_version('/', version, printer, fancy=False, dry_run=True)
 
351
    Traceback (most recent call last):
 
352
    NotPreviousImport: / is not the location of a previous import.
 
353
    >>> import_version(result_path, version, printer, fancy=False, dry_run=True)
 
354
    Traceback (most recent call last):
 
355
    UserError: The version test@example.com/test--test--0.1 does not exist.
 
356
    >>> version = pybaz.Version("test@example.com/test--test--0")
 
357
    >>> import_version(result_path, version, printer, fancy=False, dry_run=True)
 
358
    not fancy
 
359
    ....
 
360
    Dry run, not modifying output_dir
 
361
    Cleaning up
 
362
    >>> import_version(result_path, version, printer, fancy=False)
 
363
    not fancy
 
364
    ....
 
365
    Cleaning up
 
366
    Import complete.
 
367
    >>> import_version(result_path, version, printer, fancy=False)
 
368
    Tree is up-to-date with test@example.com/test--test--0--patch-2
 
369
    >>> commit_more_test_revisions()
 
370
    >>> import_version(result_path, version, printer, fancy=False)
 
371
    not fancy
 
372
    ..
 
373
    Cleaning up
 
374
    Import complete.
 
375
    >>> teardown_environ(q)
 
376
    """
 
377
    try:
 
378
        ancestors, old_revno = get_remaining_revisions(output_dir, version,
 
379
                                                       reuse_history_from)
 
380
    except NotBranchError, e:
 
381
        raise NotPreviousImport(e.path)
 
382
    if old_revno is None and len(ancestors) == 0:
 
383
        print 'Version %s has no revisions.' % version
 
384
        return
 
385
    if len(ancestors) == 0:
 
386
        last_revision = get_last_revision(Branch.open(output_dir))
 
387
        print 'Tree is up-to-date with %s' % last_revision
 
388
        return
 
389
 
 
390
    progress_bar = ProgressBar()
 
391
    tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
 
392
                               dir=os.path.dirname(output_dir))
 
393
    try:
 
394
        if not fancy:
 
395
            print "not fancy"
 
396
        try:
 
397
            for result in iter_import_version(output_dir, ancestors, tempdir,
 
398
                    progress_bar, fast=fast, verbose=verbose, dry_run=dry_run,
 
399
                    max_count=max_count):
 
400
                if fancy:
 
401
                    show_progress(progress_bar, result)
 
402
                else:
 
403
                    sys.stdout.write('.')
 
404
        finally:
 
405
            if fancy:
 
406
                progress_bar.clear()
 
407
            else:
 
408
                sys.stdout.write('\n')
 
409
 
 
410
        if dry_run:
 
411
            print 'Dry run, not modifying output_dir'
 
412
            return
 
413
        if os.path.exists(output_dir):
 
414
            # Move the bzr control directory back, and update the working tree
 
415
            revdir = os.path.join(tempdir, "rd")
 
416
            if os.path.exists(revdir):
 
417
                # actual imports were done
 
418
                tmp_bzr_dir = os.path.join(tempdir, '.bzr')
 
419
                
 
420
                bzr_dir = os.path.join(output_dir, '.bzr')
 
421
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
422
    
 
423
                os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
 
424
                os.rename(new_bzr_dir, bzr_dir)
 
425
                try:
 
426
                    WorkingTree.open_containing(output_dir)[0].revert([])
 
427
                except:
 
428
                    # If something failed, move back the original bzr directory
 
429
                    os.rename(bzr_dir, new_bzr_dir)
 
430
                    os.rename(tmp_bzr_dir, bzr_dir)
 
431
                    raise
 
432
            else:
 
433
                # no imports - perhaps just append_revisions
 
434
                # should not fail:
 
435
                WorkingTree.open_containing(output_dir)[0].revert([])
 
436
                #bzrlib.builtins.merge((output_dir, -1), (output_dir, None), # old_revno), 
 
437
                #                   check_clean=False, this_dir=output_dir, 
 
438
                #                   ignore_zero=True)
 
439
        else:
 
440
            revdir = os.path.join(tempdir, "rd")
 
441
            os.rename(revdir, output_dir)
 
442
 
 
443
    finally:
 
444
        printer('Cleaning up')
 
445
        shutil.rmtree(tempdir)
 
446
    printer("Import complete.")
 
447
            
 
448
class UserError(BzrCommandError):
 
449
    def __init__(self, message):
 
450
        """Exception to throw when a user makes an impossible request
 
451
        :param message: The message to emit when printing this exception
 
452
        :type message: string
 
453
        """
 
454
        BzrCommandError.__init__(self, message)
 
455
 
 
456
class NotPreviousImport(UserError):
 
457
    def __init__(self, path):
 
458
        UserError.__init__(self, "%s is not the location of a previous import."
 
459
                           % path)
 
460
 
 
461
 
 
462
def revision_id(arch_revision):
 
463
    """
 
464
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
 
465
    designates a revision imported with an experimental algorithm.  A number
 
466
    would indicate a particular standardized version.
 
467
 
 
468
    :param arch_revision: The Arch revision to generate an ID for.
 
469
 
 
470
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
 
471
    'Arch-1:you@example.com%cat--br--0--base-0'
 
472
    """
 
473
    return "Arch-1:%s" % str(arch_revision).replace('/', '%')
 
474
 
 
475
class NotArchRevision(Exception):
 
476
    def __init__(self, revision_id):
 
477
        msg = "The revision id %s does not look like it came from Arch."\
 
478
            % revision_id
 
479
        Exception.__init__(self, msg)
 
480
 
 
481
def arch_revision(revision_id):
 
482
    """
 
483
    >>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0"))
 
484
    Traceback (most recent call last):
 
485
    NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0 does not look like it came from Arch.
 
486
    >>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--base-5"))
 
487
    Traceback (most recent call last):
 
488
    NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
 
489
    >>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--patch-5"))
 
490
    'jrandom@example.com/test--test--0--patch-5'
 
491
    """
 
492
    if revision_id is None:
 
493
        return None
 
494
    if revision_id[:7] != 'Arch-1:':
 
495
        raise NotArchRevision(revision_id)
 
496
    else:
 
497
        try:
 
498
            return pybaz.Revision(revision_id[7:].replace('%', '/'))
 
499
        except pybaz.errors.NamespaceError, e:
 
500
            raise NotArchRevision(revision_id)
 
501
            
 
502
def iter_import_version(output_dir, ancestors, tempdir, pb, fast=False,
 
503
                        verbose=False, dry_run=False, max_count=None):
 
504
    revdir = None
 
505
 
 
506
    # Uncomment this for testing, it basically just has baz2bzr only update
 
507
    # 5 patches at a time
 
508
    if max_count:
 
509
        ancestors = ancestors[:max_count]
 
510
 
 
511
    # Not sure if I want this output. basically it tells you ahead of time
 
512
    # what it is going to do, but then later it tells you as it is doing it.
 
513
    # what probably would be best would be to collapse it into ranges, so that
 
514
    # this gives the simple view, and then later it gives the blow by blow.
 
515
    #if verbose:
 
516
    #    print 'Adding the following revisions:'
 
517
    #    for a in ancestors:
 
518
    #        print '\t%s' % a
 
519
 
 
520
    previous_version=None
 
521
    missing_ancestor = None
 
522
 
 
523
    for i in range(len(ancestors)):
 
524
        revision = ancestors[i]
 
525
        rev_id = revision_id(revision)
 
526
        direct_merges = []
 
527
        if verbose:
 
528
            version = str(revision.version)
 
529
            if version != previous_version:
 
530
                clear_progress_bar()
 
531
                print '\rOn version: %s' % version
 
532
            yield Progress(str(revision.patchlevel), i, len(ancestors))
 
533
            previous_version = version
 
534
        else:
 
535
            yield Progress("revisions", i, len(ancestors))
 
536
        if revdir is None and os.path.exists(output_dir):
 
537
            # check for imported revisions and if present just append immediately
 
538
            branch = Branch.open(output_dir)
 
539
            if branch.repository.has_revision(rev_id):
 
540
                branch.append_revision(rev_id)
 
541
                continue
 
542
        if revdir is None:
 
543
            revdir = os.path.join(tempdir, "rd")
 
544
            try:
 
545
                tree, baz_inv, log = get_revision(revdir, revision)
 
546
            except pybaz.errors.ExecProblem, e:
 
547
                if ("%s" % e.args).find('could not connect') == -1:
 
548
                    raise
 
549
                missing_ancestor = revision
 
550
                revdir = None
 
551
                print ("unable to access ancestor %s, making into a merge."
 
552
                       % missing_ancestor)
 
553
                continue
 
554
            if os.path.exists(output_dir):
 
555
                bzr_dir = os.path.join(output_dir, '.bzr')
 
556
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
557
                # This would be much faster with a simple os.rename(), but if
 
558
                # we fail, we have corrupted the original .bzr directory.  Is
 
559
                # that a big problem, as we can just back out the last
 
560
                # revisions in .bzr/revision_history I don't really know
 
561
                # RBC20051024 - yes, it would be a problem as we could not then
 
562
                # apply the corrupted revision.
 
563
                shutil.copytree(bzr_dir, new_bzr_dir)
 
564
                # Now revdir should have a tree with the latest .bzr, and the
 
565
                # next revision of the baz tree
 
566
                branch = Branch.open(revdir)
 
567
            else:
 
568
                branch = Branch.initialize(revdir)
 
569
        else:
 
570
            old = os.path.join(revdir, ".bzr")
 
571
            new = os.path.join(tempdir, ".bzr")
 
572
            os.rename(old, new)
 
573
            baz_inv, log = apply_revision(tree, revision)
 
574
            os.rename(new, old)
 
575
            branch = Branch.open(revdir)
 
576
        # cached so we can delete the log
 
577
        log_date = log.date
 
578
        log_summary = log.summary
 
579
        log_description = log.description
 
580
        is_continuation = log.continuation_of is not None
 
581
        log_creator = log.creator
 
582
        direct_merges = get_direct_merges(revdir, revision, log)
 
583
 
 
584
        timestamp = email.Utils.mktime_tz(log_date + (0,))
 
585
        if log_summary is None:
 
586
            log_summary = ""
 
587
        # log_descriptions of None and "" are ignored.
 
588
        if not is_continuation and log_description:
 
589
            log_message = "\n".join((log_summary, log_description))
 
590
        else:
 
591
            log_message = log_summary
 
592
        branch.lock_write()
 
593
        target_tree = WorkingTree(revdir ,branch=branch)
 
594
        target_tree.lock_write()
 
595
        try:
 
596
            if missing_ancestor:
 
597
                # if we want it to be in revision-history, do that here.
 
598
                target_tree.add_pending_merge(revision_id(missing_ancestor))
 
599
                missing_ancestor = None
 
600
            for merged_rev in direct_merges:
 
601
                target_tree.add_pending_merge(revision_id(merged_rev))
 
602
            target_tree.set_inventory(baz_inv)
 
603
            commitobj = Commit(reporter=ImportCommitReporter(pb))
 
604
            commitobj.commit(branch, log_message.decode('ascii', 'replace'), 
 
605
                             verbose=False, committer=log_creator,
 
606
                             timestamp=timestamp, timezone=0, rev_id=rev_id)
 
607
        finally:
 
608
            target_tree.unlock()
 
609
            branch.unlock()
 
610
    yield Progress("revisions", len(ancestors), len(ancestors))
 
611
    unlink_unversioned(branch, revdir)
 
612
 
 
613
def get_direct_merges(revdir, revision, log):
 
614
    continuation = log.continuation_of
 
615
    previous_version = revision.version
 
616
    if pybaz.WorkingTree(revdir).tree_version != previous_version:
 
617
        pybaz.WorkingTree(revdir).set_tree_version(previous_version)
 
618
    log_path = "%s/{arch}/%s/%s/%s/%s/patch-log/%s" % (revdir, 
 
619
        revision.category.nonarch, revision.branch.nonarch, 
 
620
        revision.version.nonarch, revision.archive, revision.patchlevel)
 
621
    temp_path = tempfile.mktemp(dir=os.path.dirname(revdir))
 
622
    os.rename(log_path, temp_path)
 
623
    merges = list(iter_new_merges(revdir, revision.version))
 
624
    direct = direct_merges(merges, [continuation])
 
625
    os.rename(temp_path, log_path)
 
626
    return direct
 
627
 
 
628
def unlink_unversioned(branch, revdir):
 
629
    for unversioned in branch.working_tree().extras():
 
630
        path = os.path.join(revdir, unversioned)
 
631
        if os.path.isdir(path):
 
632
            shutil.rmtree(path)
 
633
        else:
 
634
            os.unlink(path)
 
635
 
 
636
def get_log(tree, revision):
 
637
    log = pybaz.Patchlog(revision, tree=tree)
 
638
    assert str(log.revision) == str(revision), (log.revision, revision)
 
639
    return log
 
640
 
 
641
def get_revision(revdir, revision):
 
642
    tree = revision.get(revdir)
 
643
    log = get_log(tree, revision)
 
644
    try:
 
645
        return tree, bzr_inventory_data(tree), log 
 
646
    except BadFileKind, e:
 
647
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
648
 
 
649
 
 
650
def apply_revision(tree, revision):
 
651
    revision.apply(tree)
 
652
    log = get_log(tree, revision)
 
653
    try:
 
654
        return bzr_inventory_data(tree), log
 
655
    except BadFileKind, e:
 
656
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
657
 
 
658
 
 
659
class BadFileKind(Exception):
 
660
    """The file kind is not permitted in bzr inventories"""
 
661
    def __init__(self, tree_root, path, kind):
 
662
        self.tree_root = tree_root
 
663
        self.path = path
 
664
        self.kind = kind
 
665
        Exception.__init__(self, "File %s is of forbidden type %s" %
 
666
                           (os.path.join(tree_root, path), kind))
 
667
 
 
668
 
 
669
def bzr_inventory_data(tree):
 
670
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
 
671
    inv_map = {}
 
672
    for arch_id, path in inv_iter:
 
673
        bzr_file_id = map_file_id(arch_id)
 
674
        inv_map[path] = bzr_file_id 
 
675
 
 
676
    bzr_inv = []
 
677
    for path, file_id in inv_map.iteritems():
 
678
        full_path = os.path.join(tree, path)
 
679
        kind = bzrlib.osutils.file_kind(full_path)
 
680
        if kind not in ("file", "directory", "symlink"):
 
681
            raise BadFileKind(tree, path, kind)
 
682
        parent_dir = os.path.dirname(path)
 
683
        if parent_dir != "":
 
684
            parent_id = inv_map[parent_dir]
 
685
        else:
 
686
            parent_id = bzrlib.inventory.ROOT_ID
 
687
        bzr_inv.append((path, file_id, parent_id, kind))
 
688
    bzr_inv.sort()
 
689
    return bzr_inv
 
690
 
 
691
_global_option('max-count', type = int)
 
692
class cmd_baz_import_branch(Command):
 
693
    """Import an Arch or Baz branch into a bzr branch"""
 
694
    takes_args = ['to_location', 'from_branch?', 'reuse_history*']
 
695
    takes_options = ['verbose', 'max-count']
 
696
 
 
697
    def printer(self, name):
 
698
        print name
 
699
 
 
700
    def run(self, to_location, from_branch=None, fast=False, max_count=None,
 
701
            verbose=False, dry_run=False, reuse_history_list=[]):
 
702
        to_location = os.path.realpath(str(to_location))
 
703
        if from_branch is not None:
 
704
            try:
 
705
                from_branch = pybaz.Version(from_branch)
 
706
            except pybaz.errors.NamespaceError:
 
707
                print "%s is not a valid Arch branch." % from_branch
 
708
                return 1
 
709
        if reuse_history_list is None:
 
710
            reuse_history_list = []
 
711
        import_version(to_location, from_branch, self.printer, 
 
712
                       max_count=max_count, 
 
713
                       reuse_history_from=reuse_history_list)
 
714
 
 
715
 
 
716
class NotInABranch(Exception):
 
717
    def __init__(self, path):
 
718
        Exception.__init__(self, "%s is not in a branch." % path)
 
719
        self.path = path
 
720
 
 
721
 
 
722
class cmd_baz_import(Command):
 
723
    """Import an Arch or Baz archive into bzr branches.
 
724
    
 
725
    reuse_history allows you to specify any previous imports you 
 
726
    have done of different archives, which this archive has branches
 
727
    tagged from. This will dramatically reduce the time to convert 
 
728
    the archive as it will not have to convert the history already
 
729
    converted in that other branch.
 
730
    """
 
731
    takes_args = ['to_root_dir', 'from_archive', 'reuse_history*']
 
732
    takes_options = ['verbose']
 
733
 
 
734
    def printer(self, name):
 
735
        print name
 
736
 
 
737
    def run(self, to_root_dir, from_archive, verbose=False,
 
738
            reuse_history_list=[]):
 
739
        if reuse_history_list is None:
 
740
            reuse_history_list = []
 
741
        to_root = str(os.path.realpath(to_root_dir))
 
742
        if not os.path.exists(to_root):
 
743
            os.mkdir(to_root)
 
744
        import_archive(to_root, from_archive, verbose, self.printer, 
 
745
                       reuse_history_list)
 
746
 
 
747
 
 
748
def import_archive(to_root, from_archive, verbose, printer,
 
749
                   reuse_history_from=[]):
 
750
    real_to = os.path.realpath(to_root)
 
751
    history_locations = [real_to] + reuse_history_from
 
752
    for version in pybaz.Archive(str(from_archive)).iter_versions():
 
753
        target = os.path.join(to_root, map_namespace(version))
 
754
        printer("importing %s into %s" % (version, target))
 
755
        if not os.path.exists(os.path.dirname(target)):
 
756
            os.makedirs(os.path.dirname(target))
 
757
        try:
 
758
            import_version(target, version, printer,
 
759
                           reuse_history_from=reuse_history_from)
 
760
        except pybaz.errors.ExecProblem,e:
 
761
            if str(e).find('The requested revision cannot be built.') != -1:
 
762
                printer("Skipping version %s as it cannot be built due"
 
763
                        " to a missing parent archive." % version)
 
764
            else:
 
765
                raise
 
766
        except UserError, e:
 
767
            if str(e).find('already exists, and the last revision ') != -1:
 
768
                printer("Skipping version %s as it has had commits made"
 
769
                        " since it was converted to bzr." % version)
 
770
            else:
 
771
                raise
 
772
 
 
773
 
 
774
def map_namespace(a_version):
 
775
    a_version = pybaz.Version("%s" % a_version)
 
776
    parser = NameParser(a_version)
 
777
    version = parser.get_version()
 
778
    branch = parser.get_branch()
 
779
    category = parser.get_category()
 
780
    if branch is None or branch == '':
 
781
        branch = "+trunk"
 
782
    if version == '0':
 
783
        return "%s/%s" % (category, branch)
 
784
    return "%s/%s/%s" % (category, version, branch)
 
785
 
 
786
def map_file_id(file_id):
 
787
    """Convert a baz file id to a bzr one."""
 
788
    return file_id.replace('%', '%25').replace('/', '%2f')