~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz2bzr

  • Committer: Aaron Bentley
  • Date: 2005-06-08 15:02:28 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050608150228-4d9daad616b1213e
Continued symlink fix, cleaned up merge, error message

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
# Copyright (C) 2005 Aaron Bentley
 
4
# <aaron.bentley@utoronto.ca>
 
5
#
 
6
#    This program is free software; you can redistribute it and/or modify
 
7
#    it under the terms of the GNU General Public License as published by
 
8
#    the Free Software Foundation; either version 2 of the License, or
 
9
#    (at your option) any later version.
 
10
#
 
11
#    This program is distributed in the hope that it will be useful,
 
12
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
#    GNU General Public License for more details.
 
15
#
 
16
#    You should have received a copy of the GNU General Public License
 
17
#    along with this program; if not, write to the Free Software
 
18
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
19
 
 
20
try:
 
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
    sys.exit(1)
 
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
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
            os.rename(revdir, output_dir)
 
274
 
 
275
    finally:
 
276
        if fancy:
 
277
            clear_progress_bar()
 
278
        else:
 
279
            sys.stdout.write('\n')
 
280
        print 'Cleaning up'
 
281
        shutil.rmtree(tempdir)
 
282
    print "Import complete."
 
283
            
 
284
class UserError(Exception):
 
285
    def __init__(self, message):
 
286
        """Exception to throw when a user makes an impossible request
 
287
        :param message: The message to emit when printing this exception
 
288
        :type message: string
 
289
        """
 
290
        Exception.__init__(self, message)
 
291
 
 
292
def revision_id(arch_revision):
 
293
    """
 
294
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
 
295
    designates a revision imported with an experimental algorithm.  A number
 
296
    would indicate a particular standardized version.
 
297
 
 
298
    :param arch_revision: The Arch revision to generate an ID for.
 
299
 
 
300
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
 
301
    'Arch-x:you@example.com%cat--br--0--base-0'
 
302
    """
 
303
    return "Arch-x:%s" % str(arch_revision).replace('/', '%')
 
304
 
 
305
class NotArchRevision(Exception):
 
306
    def __init__(self, revision_id):
 
307
        msg = "The revision id %s does not look like it came from Arch."\
 
308
            % revision_id
 
309
        Exception.__init__(self, msg)
 
310
 
 
311
def arch_revision(revision_id):
 
312
    """
 
313
    >>> str(arch_revision("Arch-x:jrandom@example.com/test--test--0"))
 
314
    'jrandom@example.com/test--test--0'
 
315
    """
 
316
    if revision_id is None:
 
317
        return None
 
318
    if revision_id[:7] != 'Arch-x:':
 
319
        raise NotArchRevision(revision_id)
 
320
    else:
 
321
        try:
 
322
            return pybaz.Revision(revision_id[7:].replace('%', '/'))
 
323
        except pybaz.errors.NamespaceError, e:
 
324
            raise NotArchRevision(revision_id)
 
325
            
 
326
def iter_import_version(output_dir, ancestors, tempdir, fast=False,
 
327
                        verbose=False, dry_run=False, max_count=None,
 
328
                        skip_symlinks=False):
 
329
    revdir = None
 
330
 
 
331
    # Uncomment this for testing, it basically just has baz2bzr only update
 
332
    # 5 patches at a time
 
333
    if max_count:
 
334
        ancestors = ancestors[:max_count]
 
335
 
 
336
    # Not sure if I want this output. basically it tells you ahead of time
 
337
    # what it is going to do, but then later it tells you as it is doing it.
 
338
    # what probably would be best would be to collapse it into ranges, so that
 
339
    # this gives the simple view, and then later it gives the blow by blow.
 
340
    #if verbose:
 
341
    #    print 'Adding the following revisions:'
 
342
    #    for a in ancestors:
 
343
    #        print '\t%s' % a
 
344
 
 
345
    previous_version=None
 
346
 
 
347
    for i in range(len(ancestors)):
 
348
        revision = ancestors[i]
 
349
        if verbose:
 
350
            version = str(revision.version)
 
351
            if version != previous_version:
 
352
                clear_progress_bar()
 
353
                print '\rOn version: %s' % version
 
354
            yield Progress(str(revision.patchlevel), i, len(ancestors))
 
355
            previous_version = version
 
356
        else:
 
357
            yield Progress("revisions", i, len(ancestors))
 
358
        if revdir is None:
 
359
            revdir = os.path.join(tempdir, "rd")
 
360
            baz_inv, log = get_revision(revdir, revision, 
 
361
                                        skip_symlinks=skip_symlinks)
 
362
            if os.path.exists(output_dir):
 
363
                bzr_dir = os.path.join(output_dir, '.bzr')
 
364
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
365
                # This would be much faster with a simple os.rename(), but if
 
366
                # we fail, we have corrupted the original .bzr directory.  Is
 
367
                # that a big problem, as we can just back out the last
 
368
                # revisions in .bzr/revision_history I don't really know
 
369
                shutil.copytree(bzr_dir, new_bzr_dir)
 
370
                # Now revdir should have a tree with the latest .bzr, and the
 
371
                # next revision of the baz tree
 
372
                branch = bzrlib.Branch(revdir)
 
373
            else:
 
374
                branch = bzrlib.Branch(revdir, init=True)
 
375
        else:
 
376
            old = os.path.join(revdir, ".bzr")
 
377
            new = os.path.join(tempdir, ".bzr")
 
378
            os.rename(old, new)
 
379
            baz_inv, log = apply_revision(revdir, revision, 
 
380
                                          skip_symlinks=skip_symlinks)
 
381
            os.rename(new, old)
 
382
            branch = bzrlib.Branch(revdir)
 
383
        timestamp = email.Utils.mktime_tz(log.date + (0,))
 
384
        rev_id = revision_id(revision)
 
385
        branch.lock_write()
 
386
        try:
 
387
            branch.set_inventory(baz_inv)
 
388
            bzrlib.trace.silent = True
 
389
            branch.commit(log.summary, verbose=False, committer=log.creator,
 
390
                          timestamp=timestamp, timezone=0, rev_id=rev_id)
 
391
        finally:
 
392
            bzrlib.trace.silent = False   
 
393
            branch.unlock()
 
394
    yield Progress("revisions", len(ancestors), len(ancestors))
 
395
    unlink_unversioned(branch, revdir)
 
396
 
 
397
def unlink_unversioned(branch, revdir):
 
398
    for unversioned in branch.working_tree().extras():
 
399
        path = os.path.join(revdir, unversioned)
 
400
        if os.path.isdir(path):
 
401
            shutil.rmtree(path)
 
402
        else:
 
403
            os.unlink(path)
 
404
 
 
405
def get_log(tree, revision):
 
406
    log = tree.iter_logs(version=revision.version, reverse=True).next()
 
407
    assert log.revision == revision
 
408
    return log
 
409
 
 
410
def get_revision(revdir, revision, skip_symlinks=False):
 
411
    revision.get(revdir)
 
412
    tree = pybaz.tree_root(revdir)
 
413
    log = get_log(tree, revision)
 
414
    try:
 
415
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log 
 
416
    except BadFileKind, e:
 
417
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
418
 
 
419
 
 
420
def apply_revision(revdir, revision, skip_symlinks=False):
 
421
    tree = pybaz.tree_root(revdir)
 
422
    revision.apply(tree)
 
423
    log = get_log(tree, revision)
 
424
    try:
 
425
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log
 
426
    except BadFileKind, e:
 
427
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
428
 
 
429
 
 
430
 
 
431
 
 
432
class BadFileKind(Exception):
 
433
    """The file kind is not permitted in bzr inventories"""
 
434
    def __init__(self, tree_root, path, kind):
 
435
        self.tree_root = tree_root
 
436
        self.path = path
 
437
        self.kind = kind
 
438
        Exception.__init__(self, "File %s is of forbidden type %s" %
 
439
                           (os.path.join(tree_root, path), kind))
 
440
 
 
441
def bzr_inventory_data(tree, skip_symlinks=False):
 
442
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
 
443
    inv_map = {}
 
444
    for file_id, path in inv_iter:
 
445
        inv_map[path] = file_id 
 
446
 
 
447
    bzr_inv = []
 
448
    for path, file_id in inv_map.iteritems():
 
449
        full_path = os.path.join(tree, path)
 
450
        kind = bzrlib.osutils.file_kind(full_path)
 
451
        if skip_symlinks and kind == "symlink":
 
452
            continue
 
453
        if kind not in ("file", "directory"):
 
454
            raise BadFileKind(tree, path, kind)
 
455
        parent_dir = os.path.dirname(path)
 
456
        if parent_dir != "":
 
457
            parent_id = inv_map[parent_dir]
 
458
        else:
 
459
            parent_id = bzrlib.inventory.ROOT_ID
 
460
        bzr_inv.append((path, file_id, parent_id, kind))
 
461
    bzr_inv.sort()
 
462
    return bzr_inv
 
463
 
 
464
def main(args):
 
465
    """Just the main() function for this script.
 
466
 
 
467
    By separating it into a function, this can be called as a child from some other
 
468
    script.
 
469
 
 
470
    :param args: The arguments to this script. Essentially sys.argv[1:]
 
471
    """
 
472
    import optparse
 
473
    parser = optparse.OptionParser(usage='%prog [options] [VERSION] OUTDIR'
 
474
        '\n  VERSION is the arch version to import.'
 
475
        '\n  OUTDIR can be an existing directory to be updated'
 
476
        '\n         or a new directory which will be created from scratch.')
 
477
    parser.add_option('--verbose', action='store_true'
 
478
        , help='Get chatty')
 
479
 
 
480
    parser.add_option('--skip-symlinks', action="store_true", 
 
481
                      dest="skip_symlinks", 
 
482
                      help="Ignore any symlinks present in the Arch tree.")
 
483
 
 
484
    g = optparse.OptionGroup(parser, 'Test options', 'Options useful while testing process.')
 
485
    g.add_option('--test', action='store_true'
 
486
        , help='Run the self-tests and exit.')
 
487
    g.add_option('--dry-run', action='store_true'
 
488
        , help='Do the update, but don\'t copy the result to OUTDIR')
 
489
    g.add_option('--max-count', type='int', metavar='COUNT', default=None
 
490
        , help='At most, add COUNT patches.')
 
491
    g.add_option('--safe', action='store_false', dest='fast')
 
492
    g.add_option('--fast', action='store_true', default=False
 
493
        , help='By default the .bzr control directory will be copied, so that an error'
 
494
        ' does not modify the original. --fast allows the directory to be renamed instead.')
 
495
    parser.add_option_group(g)
 
496
 
 
497
    (opts, args) = parser.parse_args(args)
 
498
 
 
499
    if opts.test:
 
500
        print "Running tests"
 
501
        import doctest
 
502
        nfail, ntests = doctest.testmod(verbose=opts.verbose)
 
503
        if nfail > 0:
 
504
            return 1
 
505
        else:
 
506
            return 0
 
507
    if len(args) == 2:
 
508
        output_dir = os.path.realpath(args[1]) 
 
509
        version = pybaz.Version(args[0])
 
510
    elif len(args) == 1:
 
511
        output_dir = os.path.realpath(args[0])
 
512
        version = None
 
513
    else:
 
514
        print 'Invalid number of arguments, try --help for more info'
 
515
        return 1
 
516
        
 
517
    try:
 
518
        
 
519
        import_version(output_dir, version,
 
520
            verbose=opts.verbose, fast=opts.fast,
 
521
            dry_run=opts.dry_run, max_count=opts.max_count,
 
522
            skip_symlinks=opts.skip_symlinks)
 
523
        return 0
 
524
    except UserError, e:
 
525
        print e
 
526
        return 1
 
527
    except KeyboardInterrupt:
 
528
        print "Aborted."
 
529
        return 1
 
530
 
 
531
 
 
532
if __name__ == '__main__':
 
533
    sys.exit(main(sys.argv[1:]))
 
534