~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to baz2bzr

  • Committer: Aaron Bentley
  • Date: 2005-06-07 18:52:04 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050607185204-5fc1f0e3d393b909
Added NEWS, obsoleted bzr-pull

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_remaining_revisions(output_dir, version):
 
176
    last_patch = None
 
177
    old_revno = None
 
178
    if os.path.exists(output_dir):
 
179
        # We are starting from an existing directory, figure out what
 
180
        # the current version is
 
181
        branch = bzrlib.Branch(output_dir)
 
182
        last_patch = branch.last_patch()
 
183
        try:
 
184
            last_patch = arch_revision(last_patch)
 
185
        except NotArchRevision:
 
186
            raise UserError(
 
187
                "Directory \"%s\" already exists, and the last revision is not"
 
188
                " an Arch revision (%s)" % (output_dir, last_patch))
 
189
        if version is None:
 
190
            version = last_patch.version
 
191
    elif version is None:
 
192
        raise UserError("No version specified, and directory does not exist.")
 
193
 
 
194
    ancestors = version_ancestry(version)
 
195
 
 
196
    if last_patch:
 
197
        for i in range(len(ancestors)):
 
198
            if ancestors[i] == last_patch:
 
199
                break
 
200
        else:
 
201
            raise UserError("Directory \"%s\" already exists, and the last "
 
202
                "revision (%s) is not in the ancestry of %s" % 
 
203
                (output_dir, last_patch, version))
 
204
        # Strip off all of the ancestors which are already present
 
205
        # And get a directory starting with the latest ancestor
 
206
        latest_ancestor = ancestors[i]
 
207
        old_revno = bzrlib.Branch(output_dir).revno()
 
208
        ancestors = ancestors[i+1:]
 
209
    return ancestors, old_revno
 
210
 
 
211
def import_version(output_dir, version, fancy=True, fast=False, verbose=False, 
 
212
                   dry_run=False, max_count=None, skip_symlinks=False):
 
213
    """
 
214
    >>> q = test_environ()
 
215
    >>> result_path = os.path.join(q, "result")
 
216
    >>> commit_test_revisions()
 
217
    >>> version = pybaz.Version("test@example.com/test--test--0")
 
218
    >>> import_version(result_path, version, fancy=False)
 
219
    not fancy
 
220
    ....
 
221
    Cleaning up
 
222
    Import complete.
 
223
    >>> teardown_environ(q)
 
224
    """
 
225
    ancestors, old_revno = get_remaining_revisions(output_dir, version)
 
226
    if len(ancestors) == 0:
 
227
        print '* Tree is up-to-date with %s' % last_patch
 
228
        return 0
 
229
 
 
230
    progress_bar = ProgressBar()
 
231
    tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
 
232
                               dir=os.path.dirname(output_dir))
 
233
    try:
 
234
        if not fancy:
 
235
            print "not fancy"
 
236
        for result in iter_import_version(output_dir, ancestors, tempdir,
 
237
                fast=fast, verbose=verbose, dry_run=dry_run, 
 
238
                max_count=max_count, skip_symlinks=skip_symlinks):
 
239
            if fancy:
 
240
                progress_bar(result)
 
241
            else:
 
242
                sys.stdout.write('.')
 
243
 
 
244
        if dry_run:
 
245
            print '**Dry run, not modifying output_dir'
 
246
            return 0
 
247
        if os.path.exists(output_dir):
 
248
            # Move the bzr control directory back, and update the working tree
 
249
            tmp_bzr_dir = os.path.join(tempdir, '.bzr')
 
250
            
 
251
            bzr_dir = os.path.join(output_dir, '.bzr')
 
252
            new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
253
 
 
254
            os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
 
255
            os.rename(new_bzr_dir, bzr_dir)
 
256
            try:
 
257
                # bzrlib.merge that exists in mainline does not have a this_dir component,
 
258
                # so we have to work in the local directory
 
259
                try:
 
260
                    pwd = os.getcwd()
 
261
                    os.chdir(output_dir)
 
262
                    bzrlib.merge.merge(('.', -1), ('.', old_revno))
 
263
                finally:
 
264
                    os.chdir(pwd)
 
265
            except:
 
266
                # If something failed, move back the original bzr directory
 
267
                os.rename(bzr_dir, new_bzr_dir)
 
268
                os.rename(tmp_bzr_dir, bzr_dir)
 
269
                raise
 
270
        else:
 
271
            os.rename(revdir, output_dir)
 
272
 
 
273
    finally:
 
274
        if fancy:
 
275
            clear_progress_bar()
 
276
        else:
 
277
            sys.stdout.write('\n')
 
278
        print 'Cleaning up'
 
279
        shutil.rmtree(tempdir)
 
280
    print "Import complete."
 
281
            
 
282
class UserError(Exception):
 
283
    def __init__(self, message):
 
284
        """Exception to throw when a user makes an impossible request
 
285
        :param message: The message to emit when printing this exception
 
286
        :type message: string
 
287
        """
 
288
        Exception.__init__(self, message)
 
289
 
 
290
def revision_id(arch_revision):
 
291
    """
 
292
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
 
293
    designates a revision imported with an experimental algorithm.  A number
 
294
    would indicate a particular standardized version.
 
295
 
 
296
    :param arch_revision: The Arch revision to generate an ID for.
 
297
 
 
298
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
 
299
    'Arch-x:you@example.com%cat--br--0--base-0'
 
300
    """
 
301
    return "Arch-x:%s" % str(arch_revision).replace('/', '%')
 
302
 
 
303
class NotArchRevision(Exception):
 
304
    def __init__(self, revision_id):
 
305
        msg = "The revision id %s does not look like it came from Arch."\
 
306
            % revision_id
 
307
        Exception.__init__(self, msg)
 
308
 
 
309
def arch_revision(revision_id):
 
310
    """
 
311
    >>> str(arch_revision("Arch-x:jrandom@example.com/test--test--0"))
 
312
    'jrandom@example.com/test--test--0'
 
313
    """
 
314
    if revision_id is None:
 
315
        return None
 
316
    if revision_id[:7] != 'Arch-x:':
 
317
        raise NotArchRevision(revision_id)
 
318
    else:
 
319
        try:
 
320
            return pybaz.Revision(revision_id[7:].replace('%', '/'))
 
321
        except pybaz.errors.NamespaceError, e:
 
322
            raise NotArchRevision(revision_id)
 
323
            
 
324
def iter_import_version(output_dir, ancestors, tempdir, fast=False,
 
325
                        verbose=False, dry_run=False, max_count=None,
 
326
                        skip_symlinks=False):
 
327
    revdir = None
 
328
 
 
329
    # Uncomment this for testing, it basically just has baz2bzr only update
 
330
    # 5 patches at a time
 
331
    if max_count:
 
332
        ancestors = ancestors[:max_count]
 
333
 
 
334
    # Not sure if I want this output. basically it tells you ahead of time
 
335
    # what it is going to do, but then later it tells you as it is doing it.
 
336
    # what probably would be best would be to collapse it into ranges, so that
 
337
    # this gives the simple view, and then later it gives the blow by blow.
 
338
    #if verbose:
 
339
    #    print 'Adding the following revisions:'
 
340
    #    for a in ancestors:
 
341
    #        print '\t%s' % a
 
342
 
 
343
    previous_version=None
 
344
 
 
345
    for i in range(len(ancestors)):
 
346
        revision = ancestors[i]
 
347
        if verbose:
 
348
            version = str(revision.version)
 
349
            if version != previous_version:
 
350
                clear_progress_bar()
 
351
                print '\rOn version: %s' % version
 
352
            yield Progress(str(revision.patchlevel), i, len(ancestors))
 
353
            previous_version = version
 
354
        else:
 
355
            yield Progress("revisions", i, len(ancestors))
 
356
        if revdir is None:
 
357
            revdir = os.path.join(tempdir, "rd")
 
358
            baz_inv, log = get_revision(revdir, revision, 
 
359
                                        skip_symlinks=skip_symlinks)
 
360
            if os.path.exists(output_dir):
 
361
                bzr_dir = os.path.join(output_dir, '.bzr')
 
362
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
 
363
                # This would be much faster with a simple os.rename(), but if
 
364
                # we fail, we have corrupted the original .bzr directory.  Is
 
365
                # that a big problem, as we can just back out the last
 
366
                # revisions in .bzr/revision_history I don't really know
 
367
                shutil.copytree(bzr_dir, new_bzr_dir)
 
368
                # Now revdir should have a tree with the latest .bzr, and the
 
369
                # next revision of the baz tree
 
370
                branch = bzrlib.Branch(revdir)
 
371
            else:
 
372
                branch = bzrlib.Branch(revdir, init=True)
 
373
        else:
 
374
            old = os.path.join(revdir, ".bzr")
 
375
            new = os.path.join(tempdir, ".bzr")
 
376
            os.rename(old, new)
 
377
            baz_inv, log = apply_revision(revdir, revision, 
 
378
                                          skip_symlinks=skip_symlinks)
 
379
            os.rename(new, old)
 
380
            branch = bzrlib.Branch(revdir)
 
381
        timestamp = email.Utils.mktime_tz(log.date + (0,))
 
382
        rev_id = revision_id(revision)
 
383
        branch.lock_write()
 
384
        try:
 
385
            branch.set_inventory(baz_inv)
 
386
            bzrlib.trace.silent = True
 
387
            branch.commit(log.summary, verbose=False, committer=log.creator,
 
388
                          timestamp=timestamp, timezone=0, rev_id=rev_id)
 
389
        finally:
 
390
            bzrlib.trace.silent = False   
 
391
            branch.unlock()
 
392
    yield Progress("revisions", len(ancestors), len(ancestors))
 
393
    unlink_unversioned(branch, revdir)
 
394
 
 
395
def unlink_unversioned(branch, revdir):
 
396
    for unversioned in branch.working_tree().extras():
 
397
        path = os.path.join(revdir, unversioned)
 
398
        if os.path.isdir(path):
 
399
            shutil.rmtree(path)
 
400
        else:
 
401
            os.unlink(path)
 
402
 
 
403
def get_log(tree, revision):
 
404
    log = tree.iter_logs(version=revision.version, reverse=True).next()
 
405
    assert log.revision == revision
 
406
    return log
 
407
 
 
408
def get_revision(revdir, revision, skip_symlinks=False):
 
409
    revision.get(revdir)
 
410
    tree = pybaz.tree_root(revdir)
 
411
    log = get_log(tree, revision)
 
412
    try:
 
413
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log 
 
414
    except BadFileKind, e:
 
415
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
416
 
 
417
 
 
418
def apply_revision(revdir, revision, skip_symlinks=False):
 
419
    tree = pybaz.tree_root(revdir)
 
420
    revision.apply(tree)
 
421
    log = get_log(tree, revision)
 
422
    try:
 
423
        return bzr_inventory_data(tree, skip_symlinks=skip_symlinks), log
 
424
    except BadFileKind, e:
 
425
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
 
426
 
 
427
 
 
428
 
 
429
 
 
430
class BadFileKind(Exception):
 
431
    """The file kind is not permitted in bzr inventories"""
 
432
    def __init__(self, tree_root, path, kind):
 
433
        self.tree_root = tree_root
 
434
        self.path = path
 
435
        self.kind = kind
 
436
        Exception.__init__(self, "File %s is of forbidden type %s" %
 
437
                           (os.path.join(tree_root, path), kind))
 
438
 
 
439
def bzr_inventory_data(tree, skip_symlinks=False):
 
440
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
 
441
    inv_map = {}
 
442
    for file_id, path in inv_iter:
 
443
        inv_map[path] = file_id 
 
444
 
 
445
    bzr_inv = []
 
446
    for path, file_id in inv_map.iteritems():
 
447
        full_path = os.path.join(tree, path)
 
448
        kind = bzrlib.osutils.file_kind(full_path)
 
449
        if skip_symlinks and kind == "symlink":
 
450
            continue
 
451
        if kind not in ("file", "directory"):
 
452
            raise BadFileKind(tree, path, kind)
 
453
        parent_dir = os.path.dirname(path)
 
454
        if parent_dir != "":
 
455
            parent_id = inv_map[parent_dir]
 
456
        else:
 
457
            parent_id = bzrlib.inventory.ROOT_ID
 
458
        bzr_inv.append((path, file_id, parent_id, kind))
 
459
    bzr_inv.sort()
 
460
    return bzr_inv
 
461
 
 
462
def main(args):
 
463
    """Just the main() function for this script.
 
464
 
 
465
    By separating it into a function, this can be called as a child from some other
 
466
    script.
 
467
 
 
468
    :param args: The arguments to this script. Essentially sys.argv[1:]
 
469
    """
 
470
    import optparse
 
471
    parser = optparse.OptionParser(usage='%prog [options] [VERSION] OUTDIR'
 
472
        '\n  VERSION is the arch version to import.'
 
473
        '\n  OUTDIR can be an existing directory to be updated'
 
474
        '\n         or a new directory which will be created from scratch.')
 
475
    parser.add_option('--verbose', action='store_true'
 
476
        , help='Get chatty')
 
477
 
 
478
    parser.add_option('--skip-symlinks', action="store_true", 
 
479
                      dest="skip_symlinks", 
 
480
                      help="Ignore any symlinks present in the Arch tree.")
 
481
 
 
482
    g = optparse.OptionGroup(parser, 'Progress options', 'Control how progress is given')
 
483
    g.add_option('--not-fancy', dest='fancy', action='store_false')
 
484
    g.add_option('--fancy', dest='fancy', action='store_true', default=True
 
485
        , help='Fancy or simple progress bar. (default: fancy)')
 
486
    parser.add_option_group(g)
 
487
 
 
488
    g = optparse.OptionGroup(parser, 'Test options', 'Options useful while testing process.')
 
489
    g.add_option('--test', action='store_true'
 
490
        , help='Run the self-tests and exit.')
 
491
    g.add_option('--dry-run', action='store_true'
 
492
        , help='Do the update, but don\'t copy the result to OUTDIR')
 
493
    g.add_option('--max-count', type='int', metavar='COUNT', default=None
 
494
        , help='At most, add COUNT patches.')
 
495
    g.add_option('--safe', action='store_false', dest='fast')
 
496
    g.add_option('--fast', action='store_true', default=False
 
497
        , help='By default the .bzr control directory will be copied, so that an error'
 
498
        ' does not modify the original. --fast allows the directory to be renamed instead.')
 
499
    parser.add_option_group(g)
 
500
 
 
501
    (opts, args) = parser.parse_args(args)
 
502
 
 
503
    if opts.test:
 
504
        print "Running tests"
 
505
        import doctest
 
506
        nfail, ntests = doctest.testmod(verbose=opts.verbose)
 
507
        if nfail > 0:
 
508
            return 1
 
509
        else:
 
510
            return 0
 
511
    if len(args) == 2:
 
512
        output_dir = os.path.realpath(args[1]) 
 
513
        version = pybaz.Version(args[0])
 
514
    elif len(args) == 1:
 
515
        output_dir = os.path.realpath(args[0])
 
516
        version = None
 
517
    else:
 
518
        print 'Invalid number of arguments, try --help for more info'
 
519
        return 1
 
520
        
 
521
    try:
 
522
        
 
523
        import_version(output_dir, version,
 
524
            verbose=opts.verbose, fast=opts.fast,
 
525
            fancy=opts.fancy, dry_run=opts.dry_run,
 
526
            max_count=opts.max_count, skip_symlinks=opts.skip_symlinks)
 
527
        return 0
 
528
    except UserError, e:
 
529
        print e
 
530
        return 1
 
531
    except KeyboardInterrupt:
 
532
        print "Aborted."
 
533
        return 1
 
534
 
 
535
 
 
536
if __name__ == '__main__':
 
537
    sys.exit(main(sys.argv[1:]))
 
538