~abentley/bzrtools/bzrtools.dev

2 by abentley
Added baz2bzr file(doh!)
1
#!/usr/bin/env python
14 by abentley
GPLed the project, ignored files
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
2 by abentley
Added baz2bzr file(doh!)
20
try:
21
    import pybaz
22
except ImportError:
23
    print "This command requires PyBaz.  Please ensure that it is installed."
24
    import sys
25
    sys.exit(1)
3 by abentley
Added add_file and add_dir functions
26
from pybaz.backends.baz import null_cmd
2 by abentley
Added baz2bzr file(doh!)
27
import tempfile
28
import os
29
import os.path
30
import shutil
7 by abentley
Completed initial construction
31
import bzrlib
45 by Aaron Bentley
Silenced commits
32
import bzrlib.trace
57 by Aaron Bentley
Added John Meinel's update-with-no-{arch} patch
33
import bzrlib.merge
8 by abentley
Added ability to run from the commandline
34
import sys
37 by Aaron Bentley
More log fixups for baz2bzr
35
import email.Utils
11 by abentley
refactored out progress.py
36
from progress import *
2 by abentley
Added baz2bzr file(doh!)
37
3 by abentley
Added add_file and add_dir functions
38
def add_id(files, id=None):
39
    """Adds an explicit id to a list of files.
40
41
    :param files: the name of the file to add an id to
42
    :type files: list of str
43
    :param id: tag one file using the specified id, instead of generating id
44
    :type id: str
45
    """
46
    args = ["add-id"]
47
    if id is not None:
48
        args.extend(["--id", id])
49
    args.extend(files)
50
    return null_cmd(args)
51
2 by abentley
Added baz2bzr file(doh!)
52
def test_environ():
53
    """
54
    >>> q = test_environ()
55
    >>> os.path.exists(q)
56
    True
57
    >>> os.path.exists(os.path.join(q, "home", ".arch-params"))
58
    True
59
    >>> teardown_environ(q)
60
    >>> os.path.exists(q)
61
    False
62
    """
63
    tdir = tempfile.mkdtemp(prefix="baz2bzr-")
64
    os.environ["HOME"] = os.path.join(tdir, "home")
65
    os.mkdir(os.environ["HOME"])
3 by abentley
Added add_file and add_dir functions
66
    arch_dir = os.path.join(tdir, "archive_dir")
67
    pybaz.make_archive("test@example.com", arch_dir)
2 by abentley
Added baz2bzr file(doh!)
68
    work_dir = os.path.join(tdir, "work_dir")
69
    os.mkdir(work_dir)
70
    os.chdir(work_dir)
3 by abentley
Added add_file and add_dir functions
71
    pybaz.init_tree(work_dir, "test@example.com/test--test--0")
2 by abentley
Added baz2bzr file(doh!)
72
    lib_dir = os.path.join(tdir, "lib_dir")
73
    os.mkdir(lib_dir)
74
    pybaz.register_revision_library(lib_dir)
6 by abentley
added commit, timport, commit_test_revisions
75
    pybaz.set_my_id("Test User<test@example.org>")
2 by abentley
Added baz2bzr file(doh!)
76
    return tdir
77
5 by abentley
changed add_file, add_dir interfaces
78
def add_file(path, text, id):
3 by abentley
Added add_file and add_dir functions
79
    """
80
    >>> q = test_environ()
5 by abentley
changed add_file, add_dir interfaces
81
    >>> add_file("path with space", "text", "lalala")
3 by abentley
Added add_file and add_dir functions
82
    >>> tree = pybaz.tree_root(".")
83
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
84
    >>> ("x_lalala", "path with space") in inv
85
    True
86
    >>> teardown_environ(q)
87
    """
88
    file(path, "wb").write(text)
5 by abentley
changed add_file, add_dir interfaces
89
    add_id([path], id)
90
91
92
def add_dir(path, id):
3 by abentley
Added add_file and add_dir functions
93
    """
94
    >>> q = test_environ()
5 by abentley
changed add_file, add_dir interfaces
95
    >>> add_dir("path with\(sp) space", "lalala")
3 by abentley
Added add_file and add_dir functions
96
    >>> tree = pybaz.tree_root(".")
97
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
4 by abentley
made test case trickier, by using pika escaping sequence in filename.
98
    >>> ("x_lalala", "path with\(sp) space") in inv
3 by abentley
Added add_file and add_dir functions
99
    True
100
    >>> teardown_environ(q)
101
    """
102
    os.mkdir(path)
5 by abentley
changed add_file, add_dir interfaces
103
    add_id([path], id)
3 by abentley
Added add_file and add_dir functions
104
2 by abentley
Added baz2bzr file(doh!)
105
def teardown_environ(tdir):
106
    os.chdir("/")
107
    shutil.rmtree(tdir)
108
6 by abentley
added commit, timport, commit_test_revisions
109
def timport(tree, summary):
110
    msg = tree.log_message()
111
    msg["summary"] = summary
112
    tree.import_(msg)
113
114
def commit(tree, summary):
115
    """
116
    >>> q = test_environ()
117
    >>> tree = pybaz.tree_root(".")
118
    >>> timport(tree, "import")
119
    >>> commit(tree, "commit")
120
    >>> logs = [str(l.revision) for l in tree.iter_logs()]
121
    >>> len(logs)
122
    2
123
    >>> logs[0]
124
    'test@example.com/test--test--0--base-0'
125
    >>> logs[1]
126
    'test@example.com/test--test--0--patch-1'
127
    >>> teardown_environ(q)
128
    """
129
    msg = tree.log_message()
130
    msg["summary"] = summary
131
    tree.commit(msg)
132
133
def commit_test_revisions():
134
    """
135
    >>> q = test_environ()
136
    >>> commit_test_revisions()
137
    >>> a = pybaz.Archive("test@example.com")
138
    >>> revisions = list(a.iter_revisions("test--test--0"))
139
    >>> len(revisions)
7 by abentley
Completed initial construction
140
    3
141
    >>> str(revisions[2])
142
    'test@example.com/test--test--0--base-0'
6 by abentley
added commit, timport, commit_test_revisions
143
    >>> str(revisions[1])
7 by abentley
Completed initial construction
144
    'test@example.com/test--test--0--patch-1'
6 by abentley
added commit, timport, commit_test_revisions
145
    >>> str(revisions[0])
7 by abentley
Completed initial construction
146
    'test@example.com/test--test--0--patch-2'
6 by abentley
added commit, timport, commit_test_revisions
147
    >>> teardown_environ(q)
148
    """
149
    tree = pybaz.tree_root(".")
150
    add_file("mainfile", "void main(void){}", "mainfile by aaron")
151
    timport(tree, "Created mainfile")
152
    file("mainfile", "wb").write("or something like that")
153
    commit(tree, "altered mainfile")
7 by abentley
Completed initial construction
154
    add_file("ofile", "this is another file", "ofile by aaron")
155
    commit(tree, "altered mainfile")
156
157
def version_ancestry(version):
158
    """
159
    >>> q = test_environ()
160
    >>> commit_test_revisions()
161
    >>> version = pybaz.Version("test@example.com/test--test--0")
162
    >>> ancestors = version_ancestry(version)
163
    >>> str(ancestors[0])
164
    'test@example.com/test--test--0--base-0'
165
    >>> str(ancestors[1])
166
    'test@example.com/test--test--0--patch-1'
167
    >>> teardown_environ(q)
168
    """
35 by Aaron Bentley
Fixed, and got test cases passing
169
    revision = version.iter_revisions(reverse=True).next()
31 by Aaron Bentley
Further updates
170
    ancestors = list(revision.iter_ancestors(metoo=True))
171
    ancestors.reverse()
7 by abentley
Completed initial construction
172
    return ancestors
173
10 by abentley
Refactored, made progress bar
174
35 by Aaron Bentley
Fixed, and got test cases passing
175
def import_version(output_dir, version, fancy=True):
7 by abentley
Completed initial construction
176
    """
177
    >>> q = test_environ()
178
    >>> result_path = os.path.join(q, "result")
179
    >>> commit_test_revisions()
180
    >>> version = pybaz.Version("test@example.com/test--test--0")
35 by Aaron Bentley
Fixed, and got test cases passing
181
    >>> import_version(result_path, version, fancy=False)
182
    not fancy
183
    ....
184
    Import complete.
7 by abentley
Completed initial construction
185
    >>> teardown_environ(q)
186
    """
47 by Aaron Bentley
merged ETA changes
187
    progress_bar = ProgressBar()
51 by Aaron Bentley
Ensured temp dir is on same filesystem as output dir
188
    tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
189
                               dir=os.path.dirname(output_dir))
7 by abentley
Completed initial construction
190
    try:
35 by Aaron Bentley
Fixed, and got test cases passing
191
        if not fancy:
192
            print "not fancy"
10 by abentley
Refactored, made progress bar
193
        for result in iter_import_version(output_dir, version, tempdir):
35 by Aaron Bentley
Fixed, and got test cases passing
194
            if fancy:
195
                progress_bar(result)
196
            else:
197
                sys.stdout.write('.')
29 by Aaron Bentley
Nicer handling of unsupported file types
198
    finally:
35 by Aaron Bentley
Fixed, and got test cases passing
199
        if fancy:
200
            clear_progress_bar()
201
        else:
202
            sys.stdout.write('\n')
7 by abentley
Completed initial construction
203
        shutil.rmtree(tempdir)
29 by Aaron Bentley
Nicer handling of unsupported file types
204
    print "Import complete."
10 by abentley
Refactored, made progress bar
205
            
12 by abentley
Error early when the user specifies an output directory that already exists
206
class UserError(Exception):
207
    def __init__(self, message):
208
        """Exception to throw when a user makes an impossible request
209
        :param message: The message to emit when printing this exception
210
        :type message: string
211
        """
212
        Exception.__init__(self, message)
213
46 by Aaron Bentley
Made bzr revision ids predictable
214
def revision_id(arch_revision):
215
    """
216
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
217
    designates a revision imported with an experimental algorithm.  A number
218
    would indicate a particular standardized version.
219
220
    :param arch_revision: The Arch revision to generate an ID for.
221
222
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
223
    'Arch-x:you@example.com%cat--br--0--base-0'
224
    """
225
    return "Arch-x:%s" % str(arch_revision).replace('/', '%')
226
10 by abentley
Refactored, made progress bar
227
def iter_import_version(output_dir, version, tempdir):
228
    revdir = None
229
    ancestors = version_ancestry(version)
56 by Aaron Bentley
Applied John Meinel's 'update' patch
230
    last_patch = None
231
    if os.path.exists(output_dir):
57 by Aaron Bentley
Added John Meinel's update-with-no-{arch} patch
232
        # We are starting from an existing directory, figure out what
233
        # the current version is
234
        branch = bzrlib.Branch(output_dir)
235
        last_patch = branch.last_patch()
236
        if last_patch is not None:
56 by Aaron Bentley
Applied John Meinel's 'update' patch
237
            if last_patch[:7] != 'Arch-x:':
238
                raise UserError("Directory \"%s\" already exists, and the last patch is not an arch patch (%s)" % (output_dir, last_patch))
239
            else:
240
                last_patch = last_patch[7:].replace('%', '/')
241
242
    if last_patch:
243
        for i in range(len(ancestors)):
244
            if str(ancestors[i]) == last_patch:
245
                break
246
        else:
247
            raise UserError("Directory \"%s\" already exists, and the last patch (%s) is not in the revision history" % (output_dir, last_patch))
57 by Aaron Bentley
Added John Meinel's update-with-no-{arch} patch
248
        # Strip off all of the ancestors which are already present
249
        # And get a directory starting with the latest ancestor
250
        latest_ancestor = ancestors[i]
251
        old_revno = bzrlib.Branch(output_dir).revno()
56 by Aaron Bentley
Applied John Meinel's 'update' patch
252
        ancestors = ancestors[i+1:]
57 by Aaron Bentley
Added John Meinel's update-with-no-{arch} patch
253
56 by Aaron Bentley
Applied John Meinel's 'update' patch
254
        if len(ancestors) == 0:
255
            print '* Tree is up-to-date with %s' % last_patch
256
            return
57 by Aaron Bentley
Added John Meinel's update-with-no-{arch} patch
257
258
        revdir = os.path.join(tempdir, "rd")
259
        baz_inv, log = get_revision(revdir, latest_ancestor)
260
        bzr_dir = os.path.join(output_dir, '.bzr')
261
        new_bzr_dir = os.path.join(revdir, '.bzr')
262
        # This would be much faster with a simple os.rename(), but if we fail,
263
        # we have corrupted the original .bzr directory.
264
        # Is that a big problem, as we can just back out the last revisions in
265
        # .bzr/revision_history
266
        # I don't really know
267
        shutil.copytree(bzr_dir, new_bzr_dir)
268
        # Now revdir should have a tree with the latest .bzr, and the correct
269
        # version of the baz tree
270
56 by Aaron Bentley
Applied John Meinel's 'update' patch
271
    # Uncomment this for testing, it basically just has baz2bzr only update
272
    # 5 patches at a time
273
    #ancestors = ancestors[:5]
274
10 by abentley
Refactored, made progress bar
275
    for i in range(len(ancestors)):
276
        revision = ancestors[i]
277
        yield Progress("revisions", i, len(ancestors))
278
        if revdir is None:
279
            revdir = os.path.join(tempdir, "rd")
55 by Aaron Bentley
Skip symlinks while importing
280
            baz_inv, log = get_revision(revdir, revision, skip_symlink=True)
10 by abentley
Refactored, made progress bar
281
            branch = bzrlib.Branch(revdir, init=True)
282
        else:
283
            old = os.path.join(revdir, ".bzr")
284
            new = os.path.join(tempdir, ".bzr")
285
            os.rename(old, new)
55 by Aaron Bentley
Skip symlinks while importing
286
            baz_inv, log = apply_revision(revdir, revision, skip_symlink=True)
10 by abentley
Refactored, made progress bar
287
            os.rename(new, old)
288
            branch = bzrlib.Branch(revdir)
37 by Aaron Bentley
More log fixups for baz2bzr
289
        timestamp = email.Utils.mktime_tz(log.date + (0,))
46 by Aaron Bentley
Made bzr revision ids predictable
290
        rev_id = revision_id(revision)
52 by Aaron Bentley
Updated for new Branch locking
291
        branch.lock_write()
45 by Aaron Bentley
Silenced commits
292
        try:
52 by Aaron Bentley
Updated for new Branch locking
293
            branch.set_inventory(baz_inv)
294
            bzrlib.trace.silent = True
45 by Aaron Bentley
Silenced commits
295
            branch.commit(log.summary, verbose=False, committer=log.creator,
46 by Aaron Bentley
Made bzr revision ids predictable
296
                          timestamp=timestamp, timezone=0, rev_id=rev_id)
45 by Aaron Bentley
Silenced commits
297
        finally:
298
            bzrlib.trace.silent = False   
52 by Aaron Bentley
Updated for new Branch locking
299
            branch.unlock()
10 by abentley
Refactored, made progress bar
300
    yield Progress("revisions", len(ancestors), len(ancestors))
57 by Aaron Bentley
Added John Meinel's update-with-no-{arch} patch
301
    unlink_unversioned(branch, revdir)
302
    if os.path.exists(output_dir):
303
        # Move the bzr control directory back, and update the working tree
304
        tmp_bzr_dir = os.path.join(tempdir, '.bzr')
305
        os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
306
        os.rename(new_bzr_dir, bzr_dir)
307
        try:
308
            # bzrlib.merge that exists in mainline does not have a this_dir component,
309
            # so we have to work in the local directory
310
            try:
311
                pwd = os.getcwd()
312
                os.chdir(output_dir)
313
                bzrlib.merge.merge(('.', -1), ('.', old_revno))
314
            finally:
315
                os.chdir(pwd)
316
        except:
317
            # If something failed, move back the original bzr directory
318
            os.rename(bzr_dir, new_bzr_dir)
319
            os.rename(tmp_bzr_dir, bzr_dir)
320
            raise
321
    else:
322
        os.rename(revdir, output_dir)
10 by abentley
Refactored, made progress bar
323
324
def unlink_unversioned(branch, revdir):
325
    for unversioned in branch.working_tree().extras():
326
        path = os.path.join(revdir, unversioned)
327
        if os.path.isdir(path):
328
            shutil.rmtree(path)
329
        else:
330
            os.unlink(path)
7 by abentley
Completed initial construction
331
37 by Aaron Bentley
More log fixups for baz2bzr
332
def get_log(tree, revision):
333
    log = tree.iter_logs(version=revision.version, reverse=True).next()
334
    assert log.revision == revision
335
    return log
336
55 by Aaron Bentley
Skip symlinks while importing
337
def get_revision(revdir, revision, skip_symlink=False):
7 by abentley
Completed initial construction
338
    revision.get(revdir)
339
    tree = pybaz.tree_root(revdir)
37 by Aaron Bentley
More log fixups for baz2bzr
340
    log = get_log(tree, revision)
29 by Aaron Bentley
Nicer handling of unsupported file types
341
    try:
55 by Aaron Bentley
Skip symlinks while importing
342
        return bzr_inventory_data(tree, skip_symlink=skip_symlink), log 
29 by Aaron Bentley
Nicer handling of unsupported file types
343
    except BadFileKind, e:
344
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
345
7 by abentley
Completed initial construction
346
55 by Aaron Bentley
Skip symlinks while importing
347
def apply_revision(revdir, revision, skip_symlink=False):
13 by abentley
Used replay to get next revision
348
    tree = pybaz.tree_root(revdir)
349
    revision.apply(tree)
37 by Aaron Bentley
More log fixups for baz2bzr
350
    log = get_log(tree, revision)
29 by Aaron Bentley
Nicer handling of unsupported file types
351
    try:
55 by Aaron Bentley
Skip symlinks while importing
352
        return bzr_inventory_data(tree, skip_symlink=skip_symlink), log
29 by Aaron Bentley
Nicer handling of unsupported file types
353
    except BadFileKind, e:
354
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
355
356
357
358
359
class BadFileKind(Exception):
360
    """The file kind is not permitted in bzr inventories"""
361
    def __init__(self, tree_root, path, kind):
362
        self.tree_root = tree_root
363
        self.path = path
364
        self.kind = kind
365
        Exception.__init__(self, "File %s is of forbidden type %s" %
366
                           (os.path.join(tree_root, path), kind))
13 by abentley
Used replay to get next revision
367
55 by Aaron Bentley
Skip symlinks while importing
368
def bzr_inventory_data(tree, skip_symlink=False):
7 by abentley
Completed initial construction
369
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
370
    inv_map = {}
371
    for file_id, path in inv_iter:
8 by abentley
Added ability to run from the commandline
372
        inv_map[path] = file_id 
7 by abentley
Completed initial construction
373
374
    bzr_inv = []
8 by abentley
Added ability to run from the commandline
375
    for path, file_id in inv_map.iteritems():
29 by Aaron Bentley
Nicer handling of unsupported file types
376
        full_path = os.path.join(tree, path)
377
        kind = bzrlib.osutils.file_kind(full_path)
55 by Aaron Bentley
Skip symlinks while importing
378
        if skip_symlink and kind == "symlink":
379
            continue
29 by Aaron Bentley
Nicer handling of unsupported file types
380
        if kind not in ("file", "directory"):
381
            raise BadFileKind(tree, path, kind)
7 by abentley
Completed initial construction
382
        parent_dir = os.path.dirname(path)
383
        if parent_dir != "":
384
            parent_id = inv_map[parent_dir]
385
        else:
386
            parent_id = bzrlib.inventory.ROOT_ID
387
        bzr_inv.append((path, file_id, parent_id, kind))
388
    bzr_inv.sort()
389
    return bzr_inv
6 by abentley
added commit, timport, commit_test_revisions
390
8 by abentley
Added ability to run from the commandline
391
if len(sys.argv) == 2 and sys.argv[1] == "test":
392
    print "Running tests"
393
    import doctest
394
    doctest.testmod()
395
elif len(sys.argv) == 3:
12 by abentley
Error early when the user specifies an output directory that already exists
396
    try:
51 by Aaron Bentley
Ensured temp dir is on same filesystem as output dir
397
        output_dir = os.path.realpath(sys.argv[2])
12 by abentley
Error early when the user specifies an output directory that already exists
398
        import_version(output_dir, pybaz.Version(sys.argv[1]))
399
    except UserError, e:
400
        print e
43 by Aaron Bentley
Handled ^C nicely
401
    except KeyboardInterrupt:
402
        print "Aborted."
8 by abentley
Added ability to run from the commandline
403
else:
404
    print "usage: %s VERSION OUTDIR" % os.path.basename(sys.argv[0])