~abentley/bzrtools/bzrtools.dev

83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
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
from bzrlib import Branch
17
from bzrlib.commands import Command
100 by Aaron Bentley
Fixed up the baz-import plugin
18
19
class NoPyBaz(Exception):
20
    def __init__(self):
21
        Exception.__init__(self, "PyBaz is not installed.")
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
22
try:
23
    import pybaz
24
    import pybaz.errors
25
except ImportError:
100 by Aaron Bentley
Fixed up the baz-import plugin
26
    raise NoPyBaz
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
27
from pybaz.backends.baz import null_cmd
28
import tempfile
29
import os
30
import os.path
31
import shutil
32
import bzrlib
33
from bzrlib.errors import BzrError
34
import bzrlib.trace
35
import bzrlib.merge
97 by aaron.bentley at utoronto
Added now-required imports
36
import bzrlib.inventory
37
import bzrlib.osutils
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
38
import sys
39
import email.Utils
40
from progress import *
41
42
def add_id(files, id=None):
43
    """Adds an explicit id to a list of files.
44
45
    :param files: the name of the file to add an id to
46
    :type files: list of str
47
    :param id: tag one file using the specified id, instead of generating id
48
    :type id: str
49
    """
50
    args = ["add-id"]
51
    if id is not None:
52
        args.extend(["--id", id])
53
    args.extend(files)
54
    return null_cmd(args)
55
56
def test_environ():
57
    """
58
    >>> q = test_environ()
59
    >>> os.path.exists(q)
60
    True
61
    >>> os.path.exists(os.path.join(q, "home", ".arch-params"))
62
    True
63
    >>> teardown_environ(q)
64
    >>> os.path.exists(q)
65
    False
66
    """
67
    tdir = tempfile.mkdtemp(prefix="testdir-")
68
    os.environ["HOME"] = os.path.join(tdir, "home")
69
    os.mkdir(os.environ["HOME"])
70
    arch_dir = os.path.join(tdir, "archive_dir")
71
    pybaz.make_archive("test@example.com", arch_dir)
72
    work_dir = os.path.join(tdir, "work_dir")
73
    os.mkdir(work_dir)
74
    os.chdir(work_dir)
75
    pybaz.init_tree(work_dir, "test@example.com/test--test--0")
76
    lib_dir = os.path.join(tdir, "lib_dir")
77
    os.mkdir(lib_dir)
78
    pybaz.register_revision_library(lib_dir)
79
    pybaz.set_my_id("Test User<test@example.org>")
80
    return tdir
81
82
def add_file(path, text, id):
83
    """
84
    >>> q = test_environ()
85
    >>> add_file("path with space", "text", "lalala")
86
    >>> tree = pybaz.tree_root(".")
87
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
88
    >>> ("x_lalala", "path with space") in inv
89
    True
90
    >>> teardown_environ(q)
91
    """
92
    file(path, "wb").write(text)
93
    add_id([path], id)
94
95
96
def add_dir(path, id):
97
    """
98
    >>> q = test_environ()
99
    >>> add_dir("path with\(sp) space", "lalala")
100
    >>> tree = pybaz.tree_root(".")
101
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
102
    >>> ("x_lalala", "path with\(sp) space") in inv
103
    True
104
    >>> teardown_environ(q)
105
    """
106
    os.mkdir(path)
107
    add_id([path], id)
108
109
def teardown_environ(tdir):
110
    os.chdir("/")
111
    shutil.rmtree(tdir)
112
113
def timport(tree, summary):
114
    msg = tree.log_message()
115
    msg["summary"] = summary
116
    tree.import_(msg)
117
118
def commit(tree, summary):
119
    """
120
    >>> q = test_environ()
121
    >>> tree = pybaz.tree_root(".")
122
    >>> timport(tree, "import")
123
    >>> commit(tree, "commit")
124
    >>> logs = [str(l.revision) for l in tree.iter_logs()]
125
    >>> len(logs)
126
    2
127
    >>> logs[0]
128
    'test@example.com/test--test--0--base-0'
129
    >>> logs[1]
130
    'test@example.com/test--test--0--patch-1'
131
    >>> teardown_environ(q)
132
    """
133
    msg = tree.log_message()
134
    msg["summary"] = summary
135
    tree.commit(msg)
136
137
def commit_test_revisions():
138
    """
139
    >>> q = test_environ()
140
    >>> commit_test_revisions()
141
    >>> a = pybaz.Archive("test@example.com")
142
    >>> revisions = list(a.iter_revisions("test--test--0"))
143
    >>> len(revisions)
144
    3
145
    >>> str(revisions[2])
146
    'test@example.com/test--test--0--base-0'
147
    >>> str(revisions[1])
148
    'test@example.com/test--test--0--patch-1'
149
    >>> str(revisions[0])
150
    'test@example.com/test--test--0--patch-2'
151
    >>> teardown_environ(q)
152
    """
153
    tree = pybaz.tree_root(".")
154
    add_file("mainfile", "void main(void){}", "mainfile by aaron")
155
    timport(tree, "Created mainfile")
156
    file("mainfile", "wb").write("or something like that")
157
    commit(tree, "altered mainfile")
158
    add_file("ofile", "this is another file", "ofile by aaron")
159
    commit(tree, "altered mainfile")
160
161
162
def commit_more_test_revisions():
163
    """
164
    >>> q = test_environ()
165
    >>> commit_test_revisions()
166
    >>> commit_more_test_revisions()
167
    >>> a = pybaz.Archive("test@example.com")
168
    >>> revisions = list(a.iter_revisions("test--test--0"))
169
    >>> len(revisions)
170
    4
171
    >>> str(revisions[0])
172
    'test@example.com/test--test--0--patch-3'
173
    >>> teardown_environ(q)
174
    """
175
    tree = pybaz.tree_root(".")
176
    add_file("trainfile", "void train(void){}", "trainfile by aaron")
177
    commit(tree, "altered trainfile")
178
179
class NoSuchVersion(Exception):
180
    def __init__(self, version):
181
        Exception.__init__(self, "The version %s does not exist." % version)
182
        self.version = version
183
184
def version_ancestry(version):
185
    """
186
    >>> q = test_environ()
187
    >>> commit_test_revisions()
188
    >>> version = pybaz.Version("test@example.com/test--test--0")
189
    >>> ancestors = version_ancestry(version)
190
    >>> str(ancestors[0])
191
    'test@example.com/test--test--0--base-0'
192
    >>> str(ancestors[1])
193
    'test@example.com/test--test--0--patch-1'
194
    >>> version = pybaz.Version("test@example.com/test--test--0.5")
195
    >>> ancestors = version_ancestry(version)
196
    Traceback (most recent call last):
197
    NoSuchVersion: The version test@example.com/test--test--0.5 does not exist.
198
    >>> teardown_environ(q)
199
    """
200
    try:
201
        revision = version.iter_revisions(reverse=True).next()
202
    except:
203
        if not version.exists():
204
            raise NoSuchVersion(version)
205
        else:
206
            raise
207
    ancestors = list(revision.iter_ancestors(metoo=True))
208
    ancestors.reverse()
209
    return ancestors
210
211
def get_last_revision(branch):
212
    last_patch = branch.last_patch()
213
    try:
214
        return arch_revision(last_patch)
215
    except NotArchRevision:
216
        raise UserError(
217
            "Directory \"%s\" already exists, and the last revision is not"
218
            " an Arch revision (%s)" % (output_dir, last_patch))
219
220
221
def get_remaining_revisions(output_dir, version):
222
    last_patch = None
223
    old_revno = None
224
    if os.path.exists(output_dir):
225
        # We are starting from an existing directory, figure out what
226
        # the current version is
227
        branch = find_branch(output_dir)
228
        last_patch = get_last_revision(branch)
229
        if version is None:
230
            version = last_patch.version
231
    elif version is None:
232
        raise UserError("No version specified, and directory does not exist.")
233
234
    try:
235
        ancestors = version_ancestry(version)
236
    except NoSuchVersion, e:
237
        raise UserError(e)
238
239
    if last_patch:
240
        for i in range(len(ancestors)):
241
            if ancestors[i] == last_patch:
242
                break
243
        else:
244
            raise UserError("Directory \"%s\" already exists, and the last "
245
                "revision (%s) is not in the ancestry of %s" % 
246
                (output_dir, last_patch, version))
247
        # Strip off all of the ancestors which are already present
248
        # And get a directory starting with the latest ancestor
249
        latest_ancestor = ancestors[i]
250
        old_revno = find_branch(output_dir).revno()
251
        ancestors = ancestors[i+1:]
252
    return ancestors, old_revno
253
254
def import_version(output_dir, version, fancy=True, fast=False, verbose=False, 
255
                   dry_run=False, max_count=None, skip_symlinks=False):
256
    """
257
    >>> q = test_environ()
258
    >>> result_path = os.path.join(q, "result")
259
    >>> commit_test_revisions()
260
    >>> version = pybaz.Version("test@example.com/test--test--0.1")
261
    >>> import_version('/', version, fancy=False, dry_run=True)
262
    Traceback (most recent call last):
263
    UserError: / exists, but is not a bzr branch.
264
    >>> import_version(result_path, version, fancy=False, dry_run=True)
265
    Traceback (most recent call last):
266
    UserError: The version test@example.com/test--test--0.1 does not exist.
267
    >>> version = pybaz.Version("test@example.com/test--test--0")
268
    >>> import_version(result_path, version, fancy=False, dry_run=True)
269
    not fancy
270
    ....
271
    Dry run, not modifying output_dir
272
    Cleaning up
273
    >>> import_version(result_path, version, fancy=False)
274
    not fancy
275
    ....
276
    Cleaning up
277
    Import complete.
278
    >>> import_version(result_path, version, fancy=False)
279
    Tree is up-to-date with test@example.com/test--test--0--patch-2
280
    >>> commit_more_test_revisions()
281
    >>> import_version(result_path, version, fancy=False)
282
    not fancy
283
    ..
284
    Cleaning up
285
    Import complete.
286
    >>> teardown_environ(q)
287
    """
288
    try:
289
        ancestors, old_revno = get_remaining_revisions(output_dir, version)
290
    except NotInABranch, e:
291
        raise UserError("%s exists, but is not a bzr branch." % e.path)
292
    if len(ancestors) == 0:
293
        last_revision = get_last_revision(find_branch(output_dir))
294
        print 'Tree is up-to-date with %s' % last_revision
295
        return
296
297
    progress_bar = ProgressBar()
298
    tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
299
                               dir=os.path.dirname(output_dir))
300
    try:
301
        if not fancy:
302
            print "not fancy"
303
        try:
304
            for result in iter_import_version(output_dir, ancestors, tempdir,
305
                    fast=fast, verbose=verbose, dry_run=dry_run, 
306
                    max_count=max_count, skip_symlinks=skip_symlinks):
307
                if fancy:
308
                    progress_bar(result)
309
                else:
310
                    sys.stdout.write('.')
311
        finally:
312
            if fancy:
90 by Aaron Bentley
Adapted bzrlib's progress bar
313
                progress_bar.clear()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
314
            else:
315
                sys.stdout.write('\n')
316
317
        if dry_run:
318
            print 'Dry run, not modifying output_dir'
319
            return
320
        if os.path.exists(output_dir):
321
            # Move the bzr control directory back, and update the working tree
322
            tmp_bzr_dir = os.path.join(tempdir, '.bzr')
323
            
324
            bzr_dir = os.path.join(output_dir, '.bzr')
325
            new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
326
327
            os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
328
            os.rename(new_bzr_dir, bzr_dir)
329
            try:
330
                bzrlib.merge.merge((output_dir, -1), (output_dir, old_revno), 
331
                                   check_clean=False, this_dir=output_dir, 
332
                                   ignore_zero=True)
333
            except:
334
                # If something failed, move back the original bzr directory
335
                os.rename(bzr_dir, new_bzr_dir)
336
                os.rename(tmp_bzr_dir, bzr_dir)
337
                raise
338
        else:
339
            revdir = os.path.join(tempdir, "rd")
340
            os.rename(revdir, output_dir)
341
342
    finally:
343
        print 'Cleaning up'
344
        shutil.rmtree(tempdir)
345
    print "Import complete."
346
            
347
class UserError(Exception):
348
    def __init__(self, message):
349
        """Exception to throw when a user makes an impossible request
350
        :param message: The message to emit when printing this exception
351
        :type message: string
352
        """
353
        Exception.__init__(self, message)
354
355
def revision_id(arch_revision):
356
    """
357
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
358
    designates a revision imported with an experimental algorithm.  A number
359
    would indicate a particular standardized version.
360
361
    :param arch_revision: The Arch revision to generate an ID for.
362
363
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
364
    'Arch-x:you@example.com%cat--br--0--base-0'
365
    """
366
    return "Arch-x:%s" % str(arch_revision).replace('/', '%')
367
368
class NotArchRevision(Exception):
369
    def __init__(self, revision_id):
370
        msg = "The revision id %s does not look like it came from Arch."\
371
            % revision_id
372
        Exception.__init__(self, msg)
373
374
def arch_revision(revision_id):
375
    """
376
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0"))
377
    Traceback (most recent call last):
378
    NotArchRevision: The revision id Arch-x:jrandom@example.com%test--test--0 does not look like it came from Arch.
379
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0--base-5"))
380
    Traceback (most recent call last):
381
    NotArchRevision: The revision id Arch-x:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
382
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0--patch-5"))
383
    'jrandom@example.com/test--test--0--patch-5'
384
    """
385
    if revision_id is None:
386
        return None
387
    if revision_id[:7] != 'Arch-x:':
388
        raise NotArchRevision(revision_id)
389
    else:
390
        try:
391
            return pybaz.Revision(revision_id[7:].replace('%', '/'))
392
        except pybaz.errors.NamespaceError, e:
393
            raise NotArchRevision(revision_id)
394
            
395
def iter_import_version(output_dir, ancestors, tempdir, fast=False,
396
                        verbose=False, dry_run=False, max_count=None,
397
                        skip_symlinks=False):
398
    revdir = None
399
400
    # Uncomment this for testing, it basically just has baz2bzr only update
401
    # 5 patches at a time
402
    if max_count:
403
        ancestors = ancestors[:max_count]
404
405
    # Not sure if I want this output. basically it tells you ahead of time
406
    # what it is going to do, but then later it tells you as it is doing it.
407
    # what probably would be best would be to collapse it into ranges, so that
408
    # this gives the simple view, and then later it gives the blow by blow.
409
    #if verbose:
410
    #    print 'Adding the following revisions:'
411
    #    for a in ancestors:
412
    #        print '\t%s' % a
413
414
    previous_version=None
415
416
    for i in range(len(ancestors)):
417
        revision = ancestors[i]
418
        if verbose:
419
            version = str(revision.version)
420
            if version != previous_version:
421
                clear_progress_bar()
422
                print '\rOn version: %s' % version
423
            yield Progress(str(revision.patchlevel), i, len(ancestors))
424
            previous_version = version
425
        else:
426
            yield Progress("revisions", i, len(ancestors))
427
        if revdir is None:
428
            revdir = os.path.join(tempdir, "rd")
429
            baz_inv, log = get_revision(revdir, revision, 
430
                                        skip_symlinks=skip_symlinks)
431
            if os.path.exists(output_dir):
432
                bzr_dir = os.path.join(output_dir, '.bzr')
433
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
434
                # This would be much faster with a simple os.rename(), but if
435
                # we fail, we have corrupted the original .bzr directory.  Is
436
                # that a big problem, as we can just back out the last
437
                # revisions in .bzr/revision_history I don't really know
438
                shutil.copytree(bzr_dir, new_bzr_dir)
439
                # Now revdir should have a tree with the latest .bzr, and the
440
                # next revision of the baz tree
441
                branch = find_branch(revdir)
442
            else:
443
                branch = bzrlib.Branch(revdir, init=True)
444
        else:
445
            old = os.path.join(revdir, ".bzr")
446
            new = os.path.join(tempdir, ".bzr")
447
            os.rename(old, new)
448
            baz_inv, log = apply_revision(revdir, revision, 
449
                                          skip_symlinks=skip_symlinks)
450
            os.rename(new, old)
451
            branch = find_branch(revdir)
452
        timestamp = email.Utils.mktime_tz(log.date + (0,))
453
        rev_id = revision_id(revision)
454
        branch.lock_write()
455
        try:
456
            branch.set_inventory(baz_inv)
457
            bzrlib.trace.silent = True
458
            branch.commit(log.summary, verbose=False, committer=log.creator,
459
                          timestamp=timestamp, timezone=0, rev_id=rev_id)
460
        finally:
461
            bzrlib.trace.silent = False   
462
            branch.unlock()
463
    yield Progress("revisions", len(ancestors), len(ancestors))
464
    unlink_unversioned(branch, revdir)
465
466
def unlink_unversioned(branch, revdir):
467
    for unversioned in branch.working_tree().extras():
468
        path = os.path.join(revdir, unversioned)
469
        if os.path.isdir(path):
470
            shutil.rmtree(path)
471
        else:
472
            os.unlink(path)
473
474
def get_log(tree, revision):
475
    log = tree.iter_logs(version=revision.version, reverse=True).next()
476
    assert log.revision == revision
477
    return log
478
479
def get_revision(revdir, revision, skip_symlinks=False):
480
    revision.get(revdir)
481
    tree = pybaz.tree_root(revdir)
482
    log = get_log(tree, revision)
483
    try:
484
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log 
485
    except BadFileKind, e:
486
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
487
488
489
def apply_revision(revdir, revision, skip_symlinks=False):
490
    tree = pybaz.tree_root(revdir)
491
    revision.apply(tree)
492
    log = get_log(tree, revision)
493
    try:
494
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log
495
    except BadFileKind, e:
496
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
497
498
499
500
501
class BadFileKind(Exception):
502
    """The file kind is not permitted in bzr inventories"""
503
    def __init__(self, tree_root, path, kind):
504
        self.tree_root = tree_root
505
        self.path = path
506
        self.kind = kind
507
        Exception.__init__(self, "File %s is of forbidden type %s" %
508
                           (os.path.join(tree_root, path), kind))
509
510
def bzr_inventory_data(tree, skip_symlinks=False):
511
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
512
    inv_map = {}
513
    for file_id, path in inv_iter:
514
        inv_map[path] = file_id 
515
516
    bzr_inv = []
517
    for path, file_id in inv_map.iteritems():
518
        full_path = os.path.join(tree, path)
519
        kind = bzrlib.osutils.file_kind(full_path)
520
        if skip_symlinks and kind == "symlink":
521
            continue
522
        if kind not in ("file", "directory"):
523
            raise BadFileKind(tree, path, kind)
524
        parent_dir = os.path.dirname(path)
525
        if parent_dir != "":
526
            parent_id = inv_map[parent_dir]
527
        else:
528
            parent_id = bzrlib.inventory.ROOT_ID
529
        bzr_inv.append((path, file_id, parent_id, kind))
530
    bzr_inv.sort()
531
    return bzr_inv
532
533
class NotInABranch(Exception):
534
    def __init__(self, path):
535
        Exception.__init__(self, "%s is not in a branch." % path)
536
        self.path = path
537
538
539
def find_branch(path):
540
    """
541
    >>> find_branch('/')
542
    Traceback (most recent call last):
543
    NotInABranch: / is not in a branch.
544
    >>> sb = bzrlib.ScratchBranch()
545
    >>> isinstance(find_branch(sb.base), bzrlib.Branch)
546
    True
547
    """
548
    try:
549
        return bzrlib.Branch(path)
550
    except BzrError, e:
551
        if e.args[0].endswith("' is not in a branch"):
552
            raise NotInABranch(path)
553
554
class cmd_baz_import(Command):
555
    """Import an Arch or Baz branch into a bzr branch"""
556
    takes_args = ['to_location', 'from_branch?']
557
    takes_options = ['verbose']
558
559
    def run(self, to_location, from_branch=None, skip_symlinks=False, 
560
            fast=False, max_count=None, verbose=False, dry_run=False):
561
        to_location = os.path.realpath(str(to_location))
562
        if from_branch is not None:
563
            try:
100 by Aaron Bentley
Fixed up the baz-import plugin
564
                from_branch = pybaz.Version(from_branch)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
565
            except pybaz.errors.NamespaceError:
566
                print "%s is not a valid Arch branch." % from_branch
567
                return 1
90 by Aaron Bentley
Adapted bzrlib's progress bar
568
        import_version(to_location, from_branch)