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