~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz_import.py

  • Committer: Aaron Bentley
  • Date: 2006-05-03 20:05:46 UTC
  • mto: This revision was merged to the branch mainline in revision 366.
  • Revision ID: abentley@panoramicfeedback.com-20060503200546-83ae584b88d70a6b
Changed rpush to rspush

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')