~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz2bzr

  • Committer: Aaron Bentley
  • Date: 2005-06-08 15:31:24 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050608153124-416aa7abcf217ccc
Ensured all tests pass

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
#    along with this program; if not, write to the Free Software
18
18
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
 
20
 
import sys
21
 
from errors import NoPyBaz
22
20
try:
23
 
    from baz_import import import_version, UserError
24
 
except NoPyBaz:
25
 
    print >> sys.stderr, "This command requires PyBaz.  Please ensure that it is installed."
 
21
    import pybaz
 
22
    import pybaz.errors
 
23
except ImportError:
 
24
    print "This command requires PyBaz.  Please ensure that it is installed."
 
25
    import sys
26
26
    sys.exit(1)
27
 
 
28
 
import pybaz
 
27
from pybaz.backends.baz import null_cmd
 
28
import tempfile
 
29
import os
29
30
import os.path
 
31
import shutil
 
32
import bzrlib
 
33
import bzrlib.trace
 
34
import bzrlib.merge
 
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="baz2bzr-")
 
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
def version_ancestry(version):
 
159
    """
 
160
    >>> q = test_environ()
 
161
    >>> commit_test_revisions()
 
162
    >>> version = pybaz.Version("test@example.com/test--test--0")
 
163
    >>> ancestors = version_ancestry(version)
 
164
    >>> str(ancestors[0])
 
165
    'test@example.com/test--test--0--base-0'
 
166
    >>> str(ancestors[1])
 
167
    'test@example.com/test--test--0--patch-1'
 
168
    >>> teardown_environ(q)
 
169
    """
 
170
    revision = version.iter_revisions(reverse=True).next()
 
171
    ancestors = list(revision.iter_ancestors(metoo=True))
 
172
    ancestors.reverse()
 
173
    return ancestors
 
174
 
 
175
def get_last_revision(branch):
 
176
    last_patch = branch.last_patch()
 
177
    try:
 
178
        return arch_revision(last_patch)
 
179
    except NotArchRevision:
 
180
        raise UserError(
 
181
            "Directory \"%s\" already exists, and the last revision is not"
 
182
            " an Arch revision (%s)" % (output_dir, last_patch))
 
183
 
 
184
 
 
185
def get_remaining_revisions(output_dir, version):
 
186
    last_patch = None
 
187
    old_revno = None
 
188
    if os.path.exists(output_dir):
 
189
        # We are starting from an existing directory, figure out what
 
190
        # the current version is
 
191
        branch = bzrlib.Branch(output_dir)
 
192
        last_patch = get_last_revision(branch)
 
193
        if version is None:
 
194
            version = last_patch.version
 
195
    elif version is None:
 
196
        raise UserError("No version specified, and directory does not exist.")
 
197
 
 
198
    ancestors = version_ancestry(version)
 
199
 
 
200
    if last_patch:
 
201
        for i in range(len(ancestors)):
 
202
            if ancestors[i] == last_patch:
 
203
                break
 
204
        else:
 
205
            raise UserError("Directory \"%s\" already exists, and the last "
 
206
                "revision (%s) is not in the ancestry of %s" % 
 
207
                (output_dir, last_patch, version))
 
208
        # Strip off all of the ancestors which are already present
 
209
        # And get a directory starting with the latest ancestor
 
210
        latest_ancestor = ancestors[i]
 
211
        old_revno = bzrlib.Branch(output_dir).revno()
 
212
        ancestors = ancestors[i+1:]
 
213
    return ancestors, old_revno
 
214
 
 
215
def import_version(output_dir, version, fancy=True, fast=False, verbose=False, 
 
216
                   dry_run=False, max_count=None, skip_symlinks=False):
 
217
    """
 
218
    >>> q = test_environ()
 
219
    >>> result_path = os.path.join(q, "result")
 
220
    >>> commit_test_revisions()
 
221
    >>> version = pybaz.Version("test@example.com/test--test--0")
 
222
    >>> import_version(result_path, version, fancy=False)
 
223
    not fancy
 
224
    ....
 
225
    Cleaning up
 
226
    Import complete.
 
227
    >>> teardown_environ(q)
 
228
    """
 
229
    ancestors, old_revno = get_remaining_revisions(output_dir, version)
 
230
    if len(ancestors) == 0:
 
231
        last_revision = get_last_revision(bzrlib.Branch(output_dir))
 
232
        print '* Tree is up-to-date with %s' % last_revision
 
233
        return 0
 
234
 
 
235
    progress_bar = ProgressBar()
 
236
    tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
 
237
                               dir=os.path.dirname(output_dir))
 
238
    try:
 
239
        if not fancy:
 
240
            print "not fancy"
 
241
        for result in iter_import_version(output_dir, ancestors, tempdir,
 
242
                fast=fast, verbose=verbose, dry_run=dry_run, 
 
243
                max_count=max_count, skip_symlinks=skip_symlinks):
 
244
            if fancy:
 
245
                progress_bar(result)
 
246
            else:
 
247
                sys.stdout.write('.')
 
248
 
 
249
        if dry_run:
 
250
            print '**Dry run, not modifying output_dir'
 
251
            return 0
 
252
        if os.path.exists(output_dir):
 
253
            # Move the bzr control directory back, and update the working tree
 
254
            tmp_bzr_dir = os.path.join(tempdir, '.bzr')
 
255
            
 
256
            bzr_dir = os.path.join(output_dir, '.bzr')
 
257
            new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
258
 
 
259
            os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
 
260
            os.rename(new_bzr_dir, bzr_dir)
 
261
            try:
 
262
                # bzrlib.merge that exists in mainline does not have a this_dir component,
 
263
                # so we have to work in the local directory
 
264
                bzrlib.merge.merge(('.', -1), ('.', old_revno), 
 
265
                                   check_clean=False, this_dir=output_dir, 
 
266
                                   ignore_zero=True)
 
267
            except:
 
268
                # If something failed, move back the original bzr directory
 
269
                os.rename(bzr_dir, new_bzr_dir)
 
270
                os.rename(tmp_bzr_dir, bzr_dir)
 
271
                raise
 
272
        else:
 
273
            revdir = os.path.join(tempdir, "rd")
 
274
            os.rename(revdir, output_dir)
 
275
 
 
276
    finally:
 
277
        if fancy:
 
278
            clear_progress_bar()
 
279
        else:
 
280
            sys.stdout.write('\n')
 
281
        print 'Cleaning up'
 
282
        shutil.rmtree(tempdir)
 
283
    print "Import complete."
 
284
            
 
285
class UserError(Exception):
 
286
    def __init__(self, message):
 
287
        """Exception to throw when a user makes an impossible request
 
288
        :param message: The message to emit when printing this exception
 
289
        :type message: string
 
290
        """
 
291
        Exception.__init__(self, message)
 
292
 
 
293
def revision_id(arch_revision):
 
294
    """
 
295
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
 
296
    designates a revision imported with an experimental algorithm.  A number
 
297
    would indicate a particular standardized version.
 
298
 
 
299
    :param arch_revision: The Arch revision to generate an ID for.
 
300
 
 
301
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
 
302
    'Arch-x:you@example.com%cat--br--0--base-0'
 
303
    """
 
304
    return "Arch-x:%s" % str(arch_revision).replace('/', '%')
 
305
 
 
306
class NotArchRevision(Exception):
 
307
    def __init__(self, revision_id):
 
308
        msg = "The revision id %s does not look like it came from Arch."\
 
309
            % revision_id
 
310
        Exception.__init__(self, msg)
 
311
 
 
312
def arch_revision(revision_id):
 
313
    """
 
314
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0"))
 
315
    Traceback (most recent call last):
 
316
    NotArchRevision: The revision id Arch-x:jrandom@example.com%test--test--0 does not look like it came from Arch.
 
317
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0--base-5"))
 
318
    Traceback (most recent call last):
 
319
    NotArchRevision: The revision id Arch-x:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
 
320
    >>> str(arch_revision("Arch-x:jrandom@example.com%test--test--0--patch-5"))
 
321
    'jrandom@example.com/test--test--0--patch-5'
 
322
    """
 
323
    if revision_id is None:
 
324
        return None
 
325
    if revision_id[:7] != 'Arch-x:':
 
326
        raise NotArchRevision(revision_id)
 
327
    else:
 
328
        try:
 
329
            return pybaz.Revision(revision_id[7:].replace('%', '/'))
 
330
        except pybaz.errors.NamespaceError, e:
 
331
            raise NotArchRevision(revision_id)
 
332
            
 
333
def iter_import_version(output_dir, ancestors, tempdir, fast=False,
 
334
                        verbose=False, dry_run=False, max_count=None,
 
335
                        skip_symlinks=False):
 
336
    revdir = None
 
337
 
 
338
    # Uncomment this for testing, it basically just has baz2bzr only update
 
339
    # 5 patches at a time
 
340
    if max_count:
 
341
        ancestors = ancestors[:max_count]
 
342
 
 
343
    # Not sure if I want this output. basically it tells you ahead of time
 
344
    # what it is going to do, but then later it tells you as it is doing it.
 
345
    # what probably would be best would be to collapse it into ranges, so that
 
346
    # this gives the simple view, and then later it gives the blow by blow.
 
347
    #if verbose:
 
348
    #    print 'Adding the following revisions:'
 
349
    #    for a in ancestors:
 
350
    #        print '\t%s' % a
 
351
 
 
352
    previous_version=None
 
353
 
 
354
    for i in range(len(ancestors)):
 
355
        revision = ancestors[i]
 
356
        if verbose:
 
357
            version = str(revision.version)
 
358
            if version != previous_version:
 
359
                clear_progress_bar()
 
360
                print '\rOn version: %s' % version
 
361
            yield Progress(str(revision.patchlevel), i, len(ancestors))
 
362
            previous_version = version
 
363
        else:
 
364
            yield Progress("revisions", i, len(ancestors))
 
365
        if revdir is None:
 
366
            revdir = os.path.join(tempdir, "rd")
 
367
            baz_inv, log = get_revision(revdir, revision, 
 
368
                                        skip_symlinks=skip_symlinks)
 
369
            if os.path.exists(output_dir):
 
370
                bzr_dir = os.path.join(output_dir, '.bzr')
 
371
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
372
                # This would be much faster with a simple os.rename(), but if
 
373
                # we fail, we have corrupted the original .bzr directory.  Is
 
374
                # that a big problem, as we can just back out the last
 
375
                # revisions in .bzr/revision_history I don't really know
 
376
                shutil.copytree(bzr_dir, new_bzr_dir)
 
377
                # Now revdir should have a tree with the latest .bzr, and the
 
378
                # next revision of the baz tree
 
379
                branch = bzrlib.Branch(revdir)
 
380
            else:
 
381
                branch = bzrlib.Branch(revdir, init=True)
 
382
        else:
 
383
            old = os.path.join(revdir, ".bzr")
 
384
            new = os.path.join(tempdir, ".bzr")
 
385
            os.rename(old, new)
 
386
            baz_inv, log = apply_revision(revdir, revision, 
 
387
                                          skip_symlinks=skip_symlinks)
 
388
            os.rename(new, old)
 
389
            branch = bzrlib.Branch(revdir)
 
390
        timestamp = email.Utils.mktime_tz(log.date + (0,))
 
391
        rev_id = revision_id(revision)
 
392
        branch.lock_write()
 
393
        try:
 
394
            branch.set_inventory(baz_inv)
 
395
            bzrlib.trace.silent = True
 
396
            branch.commit(log.summary, verbose=False, committer=log.creator,
 
397
                          timestamp=timestamp, timezone=0, rev_id=rev_id)
 
398
        finally:
 
399
            bzrlib.trace.silent = False   
 
400
            branch.unlock()
 
401
    yield Progress("revisions", len(ancestors), len(ancestors))
 
402
    unlink_unversioned(branch, revdir)
 
403
 
 
404
def unlink_unversioned(branch, revdir):
 
405
    for unversioned in branch.working_tree().extras():
 
406
        path = os.path.join(revdir, unversioned)
 
407
        if os.path.isdir(path):
 
408
            shutil.rmtree(path)
 
409
        else:
 
410
            os.unlink(path)
 
411
 
 
412
def get_log(tree, revision):
 
413
    log = tree.iter_logs(version=revision.version, reverse=True).next()
 
414
    assert log.revision == revision
 
415
    return log
 
416
 
 
417
def get_revision(revdir, revision, skip_symlinks=False):
 
418
    revision.get(revdir)
 
419
    tree = pybaz.tree_root(revdir)
 
420
    log = get_log(tree, revision)
 
421
    try:
 
422
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log 
 
423
    except BadFileKind, e:
 
424
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
425
 
 
426
 
 
427
def apply_revision(revdir, revision, skip_symlinks=False):
 
428
    tree = pybaz.tree_root(revdir)
 
429
    revision.apply(tree)
 
430
    log = get_log(tree, revision)
 
431
    try:
 
432
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log
 
433
    except BadFileKind, e:
 
434
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
435
 
 
436
 
 
437
 
 
438
 
 
439
class BadFileKind(Exception):
 
440
    """The file kind is not permitted in bzr inventories"""
 
441
    def __init__(self, tree_root, path, kind):
 
442
        self.tree_root = tree_root
 
443
        self.path = path
 
444
        self.kind = kind
 
445
        Exception.__init__(self, "File %s is of forbidden type %s" %
 
446
                           (os.path.join(tree_root, path), kind))
 
447
 
 
448
def bzr_inventory_data(tree, skip_symlinks=False):
 
449
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
 
450
    inv_map = {}
 
451
    for file_id, path in inv_iter:
 
452
        inv_map[path] = file_id 
 
453
 
 
454
    bzr_inv = []
 
455
    for path, file_id in inv_map.iteritems():
 
456
        full_path = os.path.join(tree, path)
 
457
        kind = bzrlib.osutils.file_kind(full_path)
 
458
        if skip_symlinks and kind == "symlink":
 
459
            continue
 
460
        if kind not in ("file", "directory"):
 
461
            raise BadFileKind(tree, path, kind)
 
462
        parent_dir = os.path.dirname(path)
 
463
        if parent_dir != "":
 
464
            parent_id = inv_map[parent_dir]
 
465
        else:
 
466
            parent_id = bzrlib.inventory.ROOT_ID
 
467
        bzr_inv.append((path, file_id, parent_id, kind))
 
468
    bzr_inv.sort()
 
469
    return bzr_inv
30
470
 
31
471
def main(args):
32
472
    """Just the main() function for this script.
65
505
 
66
506
    if opts.test:
67
507
        print "Running tests"
68
 
        import doctest, baz_import
69
 
        nfail, ntests = doctest.testmod(baz_import, verbose=opts.verbose)
 
508
        import doctest
 
509
        nfail, ntests = doctest.testmod(verbose=opts.verbose)
70
510
        if nfail > 0:
71
511
            return 1
72
512
        else:
73
513
            return 0
74
514
    if len(args) == 2:
75
 
        version,output_dir = args
76
 
            
 
515
        output_dir = os.path.realpath(args[1]) 
 
516
        version = pybaz.Version(args[0])
77
517
    elif len(args) == 1:
78
 
        output_dir = args[0]
 
518
        output_dir = os.path.realpath(args[0])
79
519
        version = None
80
520
    else:
81
521
        print 'Invalid number of arguments, try --help for more info'
82
522
        return 1
83
 
 
84
 
    output_dir = os.path.realpath(output_dir)
85
 
    if version is not None:
86
 
        try:
87
 
            version = pybaz.Version(version)
88
 
        except pybaz.errors.NamespaceError:
89
 
            print "%s is not a valid Arch branch." % version
90
 
            return 1
91
523
        
92
524
    try:
 
525
        
93
526
        import_version(output_dir, version,
94
527
            verbose=opts.verbose, fast=opts.fast,
95
528
            dry_run=opts.dry_run, max_count=opts.max_count,
102
535
        print "Aborted."
103
536
        return 1
104
537
 
105
 
        
106
538
 
107
539
if __name__ == '__main__':
108
540
    sys.exit(main(sys.argv[1:]))