~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz_import.py

  • Committer: Aaron Bentley
  • Date: 2009-08-20 04:49:51 UTC
  • mfrom: (718.1.1 bzrtools)
  • Revision ID: aaron@aaronbentley.com-20090820044951-7x7wv9vm6f9xm1co
Merge 1.18 branch.

Show diffs side-by-side

added added

removed removed

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