~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz_import.py

  • Committer: Aaron Bentley
  • Date: 2006-03-01 16:04:07 UTC
  • mto: (147.4.30 trunk)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: abentley@panoramicfeedback.com-20060301160407-c8db0a2c02699dd2
Stopped using deprecated PyBaz functionality

Show diffs side-by-side

added added

removed removed

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