~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:
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_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
30
461
 
31
462
def main(args):
32
463
    """Just the main() function for this script.
48
479
                      dest="skip_symlinks", 
49
480
                      help="Ignore any symlinks present in the Arch tree.")
50
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
 
51
488
    g = optparse.OptionGroup(parser, 'Test options', 'Options useful while testing process.')
52
489
    g.add_option('--test', action='store_true'
53
490
        , help='Run the self-tests and exit.')
65
502
 
66
503
    if opts.test:
67
504
        print "Running tests"
68
 
        import doctest, baz_import
69
 
        nfail, ntests = doctest.testmod(baz_import, verbose=opts.verbose)
 
505
        import doctest
 
506
        nfail, ntests = doctest.testmod(verbose=opts.verbose)
70
507
        if nfail > 0:
71
508
            return 1
72
509
        else:
73
510
            return 0
74
511
    if len(args) == 2:
75
 
        version,output_dir = args
76
 
            
 
512
        output_dir = os.path.realpath(args[1]) 
 
513
        version = pybaz.Version(args[0])
77
514
    elif len(args) == 1:
78
 
        output_dir = args[0]
 
515
        output_dir = os.path.realpath(args[0])
79
516
        version = None
80
517
    else:
81
518
        print 'Invalid number of arguments, try --help for more info'
82
519
        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
520
        
92
521
    try:
 
522
        
93
523
        import_version(output_dir, version,
94
524
            verbose=opts.verbose, fast=opts.fast,
95
 
            dry_run=opts.dry_run, max_count=opts.max_count,
96
 
            skip_symlinks=opts.skip_symlinks)
 
525
            fancy=opts.fancy, dry_run=opts.dry_run,
 
526
            max_count=opts.max_count, skip_symlinks=opts.skip_symlinks)
97
527
        return 0
98
528
    except UserError, e:
99
529
        print e
102
532
        print "Aborted."
103
533
        return 1
104
534
 
105
 
        
106
535
 
107
536
if __name__ == '__main__':
108
537
    sys.exit(main(sys.argv[1:]))