~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz_import.py

  • Committer: Aaron Bentley
  • Date: 2006-03-22 15:19:16 UTC
  • Revision ID: abentley@panoramicfeedback.com-20060322151916-75711de1522d1f68
Tagged BZRTOOLS commands to reduce confusion

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