~abentley/bzrtools/bzrtools.dev

83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
1
# Copyright (C) 2005 by Aaron Bentley
2
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
147.1.2 by Robert Collins
test empty import and tagged branches
16
147.4.10 by Robert Collins
missing an errno import
17
import errno
18
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
19
from bzrlib.errors import BzrError
20
from bzrlib.errors import NotBranchError, BzrCommandError, NoSuchRevision
115 by aaron.bentley at utoronto
Import fixes from magnus@therning.org
21
from bzrlib.branch import Branch
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
22
from bzrlib.clone import copy_branch
147.2.8 by Aaron Bentley
Quieten commits
23
from bzrlib.commit import Commit, NullCommitReporter
147.1.36 by Robert Collins
updates for bzr api changes
24
from bzrlib.commands import Command
25
from bzrlib.option import _global_option
147.4.19 by Robert Collins
Update for integration move of read_working_inventory from Branch to WorkingTree.
26
from bzrlib.workingtree import WorkingTree
105 by Aaron Bentley
Fixed NoPyBaz detection
27
from errors import NoPyBaz
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
28
try:
147.2.3 by Aaron Bentley
Fixed import issues w/ PyBaz
29
    import pybaz
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
30
    import pybaz.errors
147.1.14 by Robert Collins
implement a namespace mapper
31
    from pybaz import NameParser as NameParser
147.2.3 by Aaron Bentley
Fixed import issues w/ PyBaz
32
    from pybaz.backends.baz import null_cmd
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
33
except ImportError:
147.2.3 by Aaron Bentley
Fixed import issues w/ PyBaz
34
    raise NoPyBaz
147.2.2 by Aaron Bentley
Committed debugging changes
35
from fai import iter_new_merges, direct_merges
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
36
import tempfile
37
import os
38
import os.path
39
import shutil
40
import bzrlib
41
import bzrlib.trace
42
import bzrlib.merge
97 by aaron.bentley at utoronto
Added now-required imports
43
import bzrlib.inventory
44
import bzrlib.osutils
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
45
import sys
46
import email.Utils
47
from progress import *
48
147.2.8 by Aaron Bentley
Quieten commits
49
class ImportCommitReporter(NullCommitReporter):
50
    def __init__(self, pb):
51
        self.pb = pb
52
53
    def escaped(self, escape_count, message):
54
        self.pb.clear()
55
        bzrlib.trace.warning("replaced %d control characters in message" %
56
                             escape_count)
57
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
58
def add_id(files, id=None):
59
    """Adds an explicit id to a list of files.
60
61
    :param files: the name of the file to add an id to
62
    :type files: list of str
63
    :param id: tag one file using the specified id, instead of generating id
64
    :type id: str
65
    """
66
    args = ["add-id"]
67
    if id is not None:
68
        args.extend(["--id", id])
69
    args.extend(files)
70
    return null_cmd(args)
71
147.1.2 by Robert Collins
test empty import and tagged branches
72
saved_dir = None
73
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
74
def test_environ():
75
    """
76
    >>> q = test_environ()
77
    >>> os.path.exists(q)
78
    True
79
    >>> os.path.exists(os.path.join(q, "home", ".arch-params"))
80
    True
81
    >>> teardown_environ(q)
82
    >>> os.path.exists(q)
83
    False
84
    """
147.1.2 by Robert Collins
test empty import and tagged branches
85
    global saved_dir
86
    saved_dir = os.getcwdu()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
87
    tdir = tempfile.mkdtemp(prefix="testdir-")
88
    os.environ["HOME"] = os.path.join(tdir, "home")
89
    os.mkdir(os.environ["HOME"])
90
    arch_dir = os.path.join(tdir, "archive_dir")
91
    pybaz.make_archive("test@example.com", arch_dir)
92
    work_dir = os.path.join(tdir, "work_dir")
93
    os.mkdir(work_dir)
94
    os.chdir(work_dir)
95
    pybaz.init_tree(work_dir, "test@example.com/test--test--0")
96
    lib_dir = os.path.join(tdir, "lib_dir")
97
    os.mkdir(lib_dir)
98
    pybaz.register_revision_library(lib_dir)
99
    pybaz.set_my_id("Test User<test@example.org>")
100
    return tdir
101
102
def add_file(path, text, id):
103
    """
104
    >>> q = test_environ()
105
    >>> add_file("path with space", "text", "lalala")
106
    >>> tree = pybaz.tree_root(".")
107
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
108
    >>> ("x_lalala", "path with space") in inv
109
    True
110
    >>> teardown_environ(q)
111
    """
112
    file(path, "wb").write(text)
113
    add_id([path], id)
114
115
116
def add_dir(path, id):
117
    """
118
    >>> q = test_environ()
119
    >>> add_dir("path with\(sp) space", "lalala")
120
    >>> tree = pybaz.tree_root(".")
121
    >>> inv = list(tree.iter_inventory_ids(source=True, both=True))
122
    >>> ("x_lalala", "path with\(sp) space") in inv
123
    True
124
    >>> teardown_environ(q)
125
    """
126
    os.mkdir(path)
127
    add_id([path], id)
128
129
def teardown_environ(tdir):
147.1.2 by Robert Collins
test empty import and tagged branches
130
    os.chdir(saved_dir)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
131
    shutil.rmtree(tdir)
132
133
def timport(tree, summary):
134
    msg = tree.log_message()
135
    msg["summary"] = summary
136
    tree.import_(msg)
137
138
def commit(tree, summary):
139
    """
140
    >>> q = test_environ()
141
    >>> tree = pybaz.tree_root(".")
142
    >>> timport(tree, "import")
143
    >>> commit(tree, "commit")
144
    >>> logs = [str(l.revision) for l in tree.iter_logs()]
145
    >>> len(logs)
146
    2
147
    >>> logs[0]
148
    'test@example.com/test--test--0--base-0'
149
    >>> logs[1]
150
    'test@example.com/test--test--0--patch-1'
151
    >>> teardown_environ(q)
152
    """
153
    msg = tree.log_message()
154
    msg["summary"] = summary
155
    tree.commit(msg)
156
157
def commit_test_revisions():
158
    """
159
    >>> q = test_environ()
160
    >>> commit_test_revisions()
161
    >>> a = pybaz.Archive("test@example.com")
162
    >>> revisions = list(a.iter_revisions("test--test--0"))
163
    >>> len(revisions)
164
    3
165
    >>> str(revisions[2])
166
    'test@example.com/test--test--0--base-0'
167
    >>> str(revisions[1])
168
    'test@example.com/test--test--0--patch-1'
169
    >>> str(revisions[0])
170
    'test@example.com/test--test--0--patch-2'
171
    >>> teardown_environ(q)
172
    """
173
    tree = pybaz.tree_root(".")
174
    add_file("mainfile", "void main(void){}", "mainfile by aaron")
175
    timport(tree, "Created mainfile")
176
    file("mainfile", "wb").write("or something like that")
177
    commit(tree, "altered mainfile")
178
    add_file("ofile", "this is another file", "ofile by aaron")
179
    commit(tree, "altered mainfile")
180
181
182
def commit_more_test_revisions():
183
    """
184
    >>> q = test_environ()
185
    >>> commit_test_revisions()
186
    >>> commit_more_test_revisions()
187
    >>> a = pybaz.Archive("test@example.com")
188
    >>> revisions = list(a.iter_revisions("test--test--0"))
189
    >>> len(revisions)
190
    4
191
    >>> str(revisions[0])
192
    'test@example.com/test--test--0--patch-3'
193
    >>> teardown_environ(q)
194
    """
195
    tree = pybaz.tree_root(".")
196
    add_file("trainfile", "void train(void){}", "trainfile by aaron")
197
    commit(tree, "altered trainfile")
198
199
class NoSuchVersion(Exception):
200
    def __init__(self, version):
201
        Exception.__init__(self, "The version %s does not exist." % version)
202
        self.version = version
203
204
def version_ancestry(version):
205
    """
206
    >>> q = test_environ()
207
    >>> commit_test_revisions()
208
    >>> version = pybaz.Version("test@example.com/test--test--0")
209
    >>> ancestors = version_ancestry(version)
210
    >>> str(ancestors[0])
211
    'test@example.com/test--test--0--base-0'
212
    >>> str(ancestors[1])
213
    'test@example.com/test--test--0--patch-1'
214
    >>> version = pybaz.Version("test@example.com/test--test--0.5")
215
    >>> ancestors = version_ancestry(version)
216
    Traceback (most recent call last):
217
    NoSuchVersion: The version test@example.com/test--test--0.5 does not exist.
218
    >>> teardown_environ(q)
219
    """
220
    try:
221
        revision = version.iter_revisions(reverse=True).next()
147.2.7 by Aaron Bentley
Handle empty versions correctly
222
    except StopIteration:
223
        return ()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
224
    except:
147.1.2 by Robert Collins
test empty import and tagged branches
225
        print version
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
226
        if not version.exists():
227
            raise NoSuchVersion(version)
228
        else:
229
            raise
230
    ancestors = list(revision.iter_ancestors(metoo=True))
231
    ancestors.reverse()
232
    return ancestors
233
234
def get_last_revision(branch):
221 by abentley
bzrtools 0.1.1 (baz-import patch)
235
    last_patch = branch.last_revision()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
236
    try:
237
        return arch_revision(last_patch)
238
    except NotArchRevision:
239
        raise UserError(
240
            "Directory \"%s\" already exists, and the last revision is not"
147.2.10 by Aaron Bentley
Improved error handling, esp when invoking on wrong branch
241
            " an Arch revision (%s)" % (branch.base, last_patch))
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
242
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
243
def do_branch(br_from, to_location, revision_id):
244
    """Derived from branch in builtins."""
245
    br_from.lock_read()
246
    try:
247
        try:
248
            os.mkdir(to_location)
249
        except OSError, e:
250
            if e.errno == errno.EEXIST:
251
                raise UserError('Target directory "%s" already'
252
                                      ' exists.' % to_location)
253
            if e.errno == errno.ENOENT:
254
                raise UserError('Parent of "%s" does not exist.' %
255
                                      to_location)
256
            else:
257
                raise
258
        try:
259
            copy_branch(br_from, to_location, revision_id, None)
260
        except NoSuchRevision:
261
            rmtree(to_location)
262
            msg = "The branch %s has no revision %s." % (from_location, revision_id)
263
            raise UserError(msg)
264
    finally:
265
        br_from.unlock()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
266
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
267
def get_remaining_revisions(output_dir, version, reuse_history_from=[]):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
268
    last_patch = None
269
    old_revno = None
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
270
    output_exists = os.path.exists(output_dir)
271
    if output_exists:
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
272
        # We are starting from an existing directory, figure out what
273
        # the current version is
147.1.29 by Robert Collins
update to latest bzr api
274
        branch = Branch.open(output_dir)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
275
        last_patch = get_last_revision(branch)
278 by Aaron Bentley
Handled cases where user supplies empty directories or branches
276
        if last_patch is None:
277
            raise NotPreviousImport(branch.base)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
278
        if version is None:
279
            version = last_patch.version
280
    elif version is None:
281
        raise UserError("No version specified, and directory does not exist.")
282
283
    try:
284
        ancestors = version_ancestry(version)
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
285
        if not output_exists and reuse_history_from != []:
286
            for ancestor in reversed(ancestors):
147.4.11 by Robert Collins
tweak to fix reusing history with more than one candidate ancestor
287
                if last_patch is not None:
288
                    # found something to copy
289
                    break
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
290
                # try to grab a copy of ancestor
291
                # note that is not optimised: we could look for namespace
292
                # transitions and only look for the past after the 
293
                # transition.
294
                for history_root in reuse_history_from:
295
                    possible_source = os.path.join(history_root,
296
                        map_namespace(ancestor.version))
297
                    try:
298
                        source = Branch.open(possible_source)
299
                        rev_id = revision_id(ancestor)
300
                        if rev_id in source.revision_history():
301
                            do_branch(source, output_dir, rev_id)
302
                            last_patch = ancestor
303
                            break
304
                    except NotBranchError:
305
                        pass
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
306
    except NoSuchVersion, e:
147.1.37 by Robert Collins
get all tests passing again, and disable importing the body of continuation log messages
307
        raise UserError(str(e))
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
308
309
    if last_patch:
310
        for i in range(len(ancestors)):
311
            if ancestors[i] == last_patch:
312
                break
313
        else:
314
            raise UserError("Directory \"%s\" already exists, and the last "
315
                "revision (%s) is not in the ancestry of %s" % 
316
                (output_dir, last_patch, version))
317
        # Strip off all of the ancestors which are already present
318
        # And get a directory starting with the latest ancestor
319
        latest_ancestor = ancestors[i]
147.1.29 by Robert Collins
update to latest bzr api
320
        old_revno = Branch.open(output_dir).revno()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
321
        ancestors = ancestors[i+1:]
322
    return ancestors, old_revno
323
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
324
325
###class Importer(object):
326
###    """An importer.
327
###    
328
###    Currently this is used as a parameter object, though more behaviour is
329
###    possible later.
330
###    """
331
###
332
###    def __init__(self, output_dir, version, printer, fancy=True, fast=False,
333
###                 verbose=False, dry_run=False, max_count=None, 
334
###                   reuse_history_from=[]):
335
###        self.output_dir = output_dir
336
###        self.version = version
337
###        self.
338
339
147.1.17 by Robert Collins
make feedback be callback based - really
340
def import_version(output_dir, version, printer, fancy=True, fast=False,
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
341
                   verbose=False, dry_run=False, max_count=None,
342
                   reuse_history_from=[]):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
343
    """
344
    >>> q = test_environ()
345
    >>> result_path = os.path.join(q, "result")
346
    >>> commit_test_revisions()
347
    >>> version = pybaz.Version("test@example.com/test--test--0.1")
147.1.20 by Robert Collins
handle missing ancestry
348
    >>> def printer(message): print message
349
    >>> import_version('/', version, printer, fancy=False, dry_run=True)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
350
    Traceback (most recent call last):
147.1.45 by Aaron Bentley
Fixed test case
351
    NotPreviousImport: / is not the location of a previous import.
147.1.20 by Robert Collins
handle missing ancestry
352
    >>> import_version(result_path, version, printer, fancy=False, dry_run=True)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
353
    Traceback (most recent call last):
354
    UserError: The version test@example.com/test--test--0.1 does not exist.
355
    >>> version = pybaz.Version("test@example.com/test--test--0")
147.1.20 by Robert Collins
handle missing ancestry
356
    >>> import_version(result_path, version, printer, fancy=False, dry_run=True)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
357
    not fancy
358
    ....
359
    Dry run, not modifying output_dir
360
    Cleaning up
147.1.20 by Robert Collins
handle missing ancestry
361
    >>> import_version(result_path, version, printer, fancy=False)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
362
    not fancy
363
    ....
364
    Cleaning up
365
    Import complete.
147.1.20 by Robert Collins
handle missing ancestry
366
    >>> import_version(result_path, version, printer, fancy=False)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
367
    Tree is up-to-date with test@example.com/test--test--0--patch-2
368
    >>> commit_more_test_revisions()
147.1.20 by Robert Collins
handle missing ancestry
369
    >>> import_version(result_path, version, printer, fancy=False)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
370
    not fancy
371
    ..
372
    Cleaning up
373
    Import complete.
374
    >>> teardown_environ(q)
375
    """
376
    try:
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
377
        ancestors, old_revno = get_remaining_revisions(output_dir, version,
378
                                                       reuse_history_from)
147.1.2 by Robert Collins
test empty import and tagged branches
379
    except NotBranchError, e:
278 by Aaron Bentley
Handled cases where user supplies empty directories or branches
380
        raise NotPreviousImport(e.path)
147.2.7 by Aaron Bentley
Handle empty versions correctly
381
    if old_revno is None and len(ancestors) == 0:
382
        print 'Version %s has no revisions.' % version
383
        return
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
384
    if len(ancestors) == 0:
147.1.29 by Robert Collins
update to latest bzr api
385
        last_revision = get_last_revision(Branch.open(output_dir))
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
386
        print 'Tree is up-to-date with %s' % last_revision
387
        return
388
389
    progress_bar = ProgressBar()
390
    tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
391
                               dir=os.path.dirname(output_dir))
392
    try:
393
        if not fancy:
394
            print "not fancy"
395
        try:
396
            for result in iter_import_version(output_dir, ancestors, tempdir,
147.2.8 by Aaron Bentley
Quieten commits
397
                    progress_bar, fast=fast, verbose=verbose, dry_run=dry_run,
147.1.6 by Robert Collins
import symlinks (needs symlink enabled bzr)
398
                    max_count=max_count):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
399
                if fancy:
102 by Aaron Bentley
Got baz2bzr/annotate working now that ProgressBar is a function
400
                    show_progress(progress_bar, result)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
401
                else:
402
                    sys.stdout.write('.')
403
        finally:
404
            if fancy:
90 by Aaron Bentley
Adapted bzrlib's progress bar
405
                progress_bar.clear()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
406
            else:
407
                sys.stdout.write('\n')
408
409
        if dry_run:
410
            print 'Dry run, not modifying output_dir'
411
            return
412
        if os.path.exists(output_dir):
413
            # Move the bzr control directory back, and update the working tree
147.4.5 by Robert Collins
When an import finds a revision it needs already in the branch, just append it to the revision history
414
            revdir = os.path.join(tempdir, "rd")
415
            if os.path.exists(revdir):
416
                # actual imports were done
417
                tmp_bzr_dir = os.path.join(tempdir, '.bzr')
418
                
419
                bzr_dir = os.path.join(output_dir, '.bzr')
420
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
421
    
422
                os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
423
                os.rename(new_bzr_dir, bzr_dir)
424
                try:
425
                    bzrlib.merge.merge((output_dir, -1), (output_dir, None), # old_revno), 
426
                                       check_clean=False, this_dir=output_dir, 
427
                                       ignore_zero=True)
428
                except:
429
                    # If something failed, move back the original bzr directory
430
                    os.rename(bzr_dir, new_bzr_dir)
431
                    os.rename(tmp_bzr_dir, bzr_dir)
432
                    raise
433
            else:
434
                # no imports - perhaps just append_revisions
435
                # should not fail:
147.1.25 by Robert Collins
when will I learn to make check first
436
                bzrlib.merge.merge((output_dir, -1), (output_dir, None), # old_revno), 
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
437
                                   check_clean=False, this_dir=output_dir, 
438
                                   ignore_zero=True)
439
        else:
440
            revdir = os.path.join(tempdir, "rd")
441
            os.rename(revdir, output_dir)
442
443
    finally:
147.1.17 by Robert Collins
make feedback be callback based - really
444
        printer('Cleaning up')
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
445
        shutil.rmtree(tempdir)
147.1.17 by Robert Collins
make feedback be callback based - really
446
    printer("Import complete.")
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
447
            
278 by Aaron Bentley
Handled cases where user supplies empty directories or branches
448
class UserError(BzrCommandError):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
449
    def __init__(self, message):
450
        """Exception to throw when a user makes an impossible request
451
        :param message: The message to emit when printing this exception
452
        :type message: string
453
        """
278 by Aaron Bentley
Handled cases where user supplies empty directories or branches
454
        BzrCommandError.__init__(self, message)
455
456
class NotPreviousImport(UserError):
457
    def __init__(self, path):
458
        UserError.__init__(self, "%s is not the location of a previous import."
459
                           % path)
460
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
461
462
def revision_id(arch_revision):
463
    """
464
    Generate a Bzr revision id from an Arch revision id.  'x' in the id
465
    designates a revision imported with an experimental algorithm.  A number
466
    would indicate a particular standardized version.
467
468
    :param arch_revision: The Arch revision to generate an ID for.
469
470
    >>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
147.1.2 by Robert Collins
test empty import and tagged branches
471
    'Arch-1:you@example.com%cat--br--0--base-0'
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
472
    """
147.1.2 by Robert Collins
test empty import and tagged branches
473
    return "Arch-1:%s" % str(arch_revision).replace('/', '%')
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
474
475
class NotArchRevision(Exception):
476
    def __init__(self, revision_id):
477
        msg = "The revision id %s does not look like it came from Arch."\
478
            % revision_id
479
        Exception.__init__(self, msg)
480
481
def arch_revision(revision_id):
482
    """
147.1.2 by Robert Collins
test empty import and tagged branches
483
    >>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0"))
484
    Traceback (most recent call last):
485
    NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0 does not look like it came from Arch.
486
    >>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--base-5"))
487
    Traceback (most recent call last):
488
    NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
489
    >>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--patch-5"))
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
490
    'jrandom@example.com/test--test--0--patch-5'
491
    """
492
    if revision_id is None:
493
        return None
147.1.2 by Robert Collins
test empty import and tagged branches
494
    if revision_id[:7] != 'Arch-1:':
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
495
        raise NotArchRevision(revision_id)
496
    else:
497
        try:
498
            return pybaz.Revision(revision_id[7:].replace('%', '/'))
499
        except pybaz.errors.NamespaceError, e:
500
            raise NotArchRevision(revision_id)
501
            
147.2.8 by Aaron Bentley
Quieten commits
502
def iter_import_version(output_dir, ancestors, tempdir, pb, fast=False,
147.1.6 by Robert Collins
import symlinks (needs symlink enabled bzr)
503
                        verbose=False, dry_run=False, max_count=None):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
504
    revdir = None
505
506
    # Uncomment this for testing, it basically just has baz2bzr only update
507
    # 5 patches at a time
508
    if max_count:
509
        ancestors = ancestors[:max_count]
510
511
    # Not sure if I want this output. basically it tells you ahead of time
512
    # what it is going to do, but then later it tells you as it is doing it.
513
    # what probably would be best would be to collapse it into ranges, so that
514
    # this gives the simple view, and then later it gives the blow by blow.
515
    #if verbose:
516
    #    print 'Adding the following revisions:'
517
    #    for a in ancestors:
518
    #        print '\t%s' % a
519
520
    previous_version=None
147.1.20 by Robert Collins
handle missing ancestry
521
    missing_ancestor = None
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
522
523
    for i in range(len(ancestors)):
524
        revision = ancestors[i]
147.4.5 by Robert Collins
When an import finds a revision it needs already in the branch, just append it to the revision history
525
        rev_id = revision_id(revision)
147.1.3 by Robert Collins
test and deliver basic pending-merges into bzr so that merging is recorded
526
        direct_merges = []
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
527
        if verbose:
528
            version = str(revision.version)
529
            if version != previous_version:
530
                clear_progress_bar()
531
                print '\rOn version: %s' % version
532
            yield Progress(str(revision.patchlevel), i, len(ancestors))
533
            previous_version = version
534
        else:
535
            yield Progress("revisions", i, len(ancestors))
147.4.5 by Robert Collins
When an import finds a revision it needs already in the branch, just append it to the revision history
536
        if revdir is None and os.path.exists(output_dir):
537
            # check for imported revisions and if present just append immediately
538
            branch = Branch.open(output_dir)
539
            if branch.has_revision(rev_id):
540
                branch.append_revision(rev_id)
541
                continue
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
542
        if revdir is None:
543
            revdir = os.path.join(tempdir, "rd")
147.1.20 by Robert Collins
handle missing ancestry
544
            try:
147.2.14 by abentley
Allouche: More efficient PyBaz use
545
                tree, baz_inv, log = get_revision(revdir, revision)
147.1.20 by Robert Collins
handle missing ancestry
546
            except pybaz.errors.ExecProblem, e:
547
                if ("%s" % e.args).find('could not connect') == -1:
548
                    raise
549
                missing_ancestor = revision
550
                revdir = None
551
                print ("unable to access ancestor %s, making into a merge."
552
                       % missing_ancestor)
553
                continue
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
554
            if os.path.exists(output_dir):
555
                bzr_dir = os.path.join(output_dir, '.bzr')
556
                new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
557
                # This would be much faster with a simple os.rename(), but if
558
                # we fail, we have corrupted the original .bzr directory.  Is
559
                # that a big problem, as we can just back out the last
560
                # revisions in .bzr/revision_history I don't really know
147.4.5 by Robert Collins
When an import finds a revision it needs already in the branch, just append it to the revision history
561
                # RBC20051024 - yes, it would be a problem as we could not then
562
                # apply the corrupted revision.
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
563
                shutil.copytree(bzr_dir, new_bzr_dir)
564
                # Now revdir should have a tree with the latest .bzr, and the
565
                # next revision of the baz tree
147.1.29 by Robert Collins
update to latest bzr api
566
                branch = Branch.open(revdir)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
567
            else:
158 by Aaron Bentley
Updated to match API changes
568
                branch = Branch.initialize(revdir)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
569
        else:
570
            old = os.path.join(revdir, ".bzr")
571
            new = os.path.join(tempdir, ".bzr")
572
            os.rename(old, new)
147.2.14 by abentley
Allouche: More efficient PyBaz use
573
            baz_inv, log = apply_revision(tree, revision)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
574
            os.rename(new, old)
147.1.29 by Robert Collins
update to latest bzr api
575
            branch = Branch.open(revdir)
147.4.2 by Robert Collins
use direct merge log always, whether its a new tree or a patch being applied
576
        # cached so we can delete the log
577
        log_date = log.date
578
        log_summary = log.summary
579
        log_description = log.description
580
        is_continuation = log.continuation_of is not None
581
        log_creator = log.creator
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
582
        direct_merges = get_direct_merges(revdir, revision, log)
147.4.2 by Robert Collins
use direct merge log always, whether its a new tree or a patch being applied
583
147.1.3 by Robert Collins
test and deliver basic pending-merges into bzr so that merging is recorded
584
        timestamp = email.Utils.mktime_tz(log_date + (0,))
147.2.5 by Aaron Bentley
Handled empty summary lines
585
        if log_summary is None:
586
            log_summary = ""
147.1.37 by Robert Collins
get all tests passing again, and disable importing the body of continuation log messages
587
        # log_descriptions of None and "" are ignored.
588
        if not is_continuation and log_description:
147.2.9 by Aaron Bentley
Fixed log conversion
589
            log_message = "\n".join((log_summary, log_description))
590
        else:
591
            log_message = log_summary
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
592
        branch.lock_write()
147.4.19 by Robert Collins
Update for integration move of read_working_inventory from Branch to WorkingTree.
593
        target_tree = WorkingTree(revdir ,branch=branch)
594
        target_tree.lock_write()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
595
        try:
147.1.20 by Robert Collins
handle missing ancestry
596
            if missing_ancestor:
597
                # if we want it to be in revision-history, do that here.
147.4.20 by Robert Collins
Branch.commit->WorkingTree.commit
598
                target_tree.add_pending_merge(revision_id(missing_ancestor))
147.1.20 by Robert Collins
handle missing ancestry
599
                missing_ancestor = None
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
600
            for merged_rev in direct_merges:
147.4.20 by Robert Collins
Branch.commit->WorkingTree.commit
601
                target_tree.add_pending_merge(revision_id(merged_rev))
147.4.19 by Robert Collins
Update for integration move of read_working_inventory from Branch to WorkingTree.
602
            target_tree.set_inventory(baz_inv)
147.2.8 by Aaron Bentley
Quieten commits
603
            commitobj = Commit(reporter=ImportCommitReporter(pb))
147.2.9 by Aaron Bentley
Fixed log conversion
604
            commitobj.commit(branch, log_message.decode('ascii', 'replace'), 
605
                             verbose=False, committer=log_creator,
606
                             timestamp=timestamp, timezone=0, rev_id=rev_id)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
607
        finally:
147.4.19 by Robert Collins
Update for integration move of read_working_inventory from Branch to WorkingTree.
608
            target_tree.unlock()
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
609
            branch.unlock()
610
    yield Progress("revisions", len(ancestors), len(ancestors))
611
    unlink_unversioned(branch, revdir)
612
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
613
def get_direct_merges(revdir, revision, log):
614
    continuation = log.continuation_of
615
    previous_version = revision.version
616
    if pybaz.WorkingTree(revdir).tree_version != previous_version:
617
        pybaz.WorkingTree(revdir).set_tree_version(previous_version)
147.1.3 by Robert Collins
test and deliver basic pending-merges into bzr so that merging is recorded
618
    log_path = "%s/{arch}/%s/%s/%s/%s/patch-log/%s" % (revdir, 
619
        revision.category.nonarch, revision.branch.nonarch, 
620
        revision.version.nonarch, revision.archive, revision.patchlevel)
147.2.13 by abentley
Allouche: always use a temp dir on the same filesystem
621
    temp_path = tempfile.mktemp(dir=os.path.dirname(revdir))
147.1.3 by Robert Collins
test and deliver basic pending-merges into bzr so that merging is recorded
622
    os.rename(log_path, temp_path)
147.1.24 by Robert Collins
trim fai cribbage
623
    merges = list(iter_new_merges(revdir, revision.version))
147.4.6 by Robert Collins
Fix continuation direct_merges output, and allow reusing history in a version import
624
    direct = direct_merges(merges, [continuation])
147.1.3 by Robert Collins
test and deliver basic pending-merges into bzr so that merging is recorded
625
    os.rename(temp_path, log_path)
626
    return direct
627
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
628
def unlink_unversioned(branch, revdir):
629
    for unversioned in branch.working_tree().extras():
630
        path = os.path.join(revdir, unversioned)
631
        if os.path.isdir(path):
632
            shutil.rmtree(path)
633
        else:
634
            os.unlink(path)
635
636
def get_log(tree, revision):
147.2.14 by abentley
Allouche: More efficient PyBaz use
637
    log = pybaz.Patchlog(revision, tree=tree)
133 by Aaron Bentley
Weakened check so baz-import works
638
    assert str(log.revision) == str(revision), (log.revision, revision)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
639
    return log
640
147.1.6 by Robert Collins
import symlinks (needs symlink enabled bzr)
641
def get_revision(revdir, revision):
147.2.14 by abentley
Allouche: More efficient PyBaz use
642
    tree = revision.get(revdir)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
643
    log = get_log(tree, revision)
644
    try:
147.2.14 by abentley
Allouche: More efficient PyBaz use
645
        return tree, bzr_inventory_data(tree), log 
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
646
    except BadFileKind, e:
647
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
648
649
147.2.14 by abentley
Allouche: More efficient PyBaz use
650
def apply_revision(tree, revision):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
651
    revision.apply(tree)
652
    log = get_log(tree, revision)
653
    try:
147.1.6 by Robert Collins
import symlinks (needs symlink enabled bzr)
654
        return bzr_inventory_data(tree), log
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
655
    except BadFileKind, e:
656
        raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
657
658
659
class BadFileKind(Exception):
660
    """The file kind is not permitted in bzr inventories"""
661
    def __init__(self, tree_root, path, kind):
662
        self.tree_root = tree_root
663
        self.path = path
664
        self.kind = kind
665
        Exception.__init__(self, "File %s is of forbidden type %s" %
666
                           (os.path.join(tree_root, path), kind))
667
147.1.29 by Robert Collins
update to latest bzr api
668
147.1.6 by Robert Collins
import symlinks (needs symlink enabled bzr)
669
def bzr_inventory_data(tree):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
670
    inv_iter = tree.iter_inventory_ids(source=True, both=True)
671
    inv_map = {}
106 by Aaron Bentley
Used limited url-encoding for file ids
672
    for arch_id, path in inv_iter:
147.4.1 by Robert Collins
test escaping of file ids is working
673
        bzr_file_id = map_file_id(arch_id)
106 by Aaron Bentley
Used limited url-encoding for file ids
674
        inv_map[path] = bzr_file_id 
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
675
676
    bzr_inv = []
677
    for path, file_id in inv_map.iteritems():
678
        full_path = os.path.join(tree, path)
679
        kind = bzrlib.osutils.file_kind(full_path)
147.1.6 by Robert Collins
import symlinks (needs symlink enabled bzr)
680
        if kind not in ("file", "directory", "symlink"):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
681
            raise BadFileKind(tree, path, kind)
682
        parent_dir = os.path.dirname(path)
683
        if parent_dir != "":
684
            parent_id = inv_map[parent_dir]
685
        else:
686
            parent_id = bzrlib.inventory.ROOT_ID
687
        bzr_inv.append((path, file_id, parent_id, kind))
688
    bzr_inv.sort()
689
    return bzr_inv
690
147.1.36 by Robert Collins
updates for bzr api changes
691
_global_option('max-count', type = int)
147.1.11 by Robert Collins
move baz-import to baz-import-branch
692
class cmd_baz_import_branch(Command):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
693
    """Import an Arch or Baz branch into a bzr branch"""
147.4.13 by Robert Collins
add a reuse_history option to the baz-import-branch command too.
694
    takes_args = ['to_location', 'from_branch?', 'reuse_history*']
147.2.10 by Aaron Bentley
Improved error handling, esp when invoking on wrong branch
695
    takes_options = ['verbose', 'max-count']
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
696
147.1.17 by Robert Collins
make feedback be callback based - really
697
    def printer(self, name):
698
        print name
699
147.1.6 by Robert Collins
import symlinks (needs symlink enabled bzr)
700
    def run(self, to_location, from_branch=None, fast=False, max_count=None,
147.4.13 by Robert Collins
add a reuse_history option to the baz-import-branch command too.
701
            verbose=False, dry_run=False, reuse_history_list=[]):
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
702
        to_location = os.path.realpath(str(to_location))
703
        if from_branch is not None:
704
            try:
100 by Aaron Bentley
Fixed up the baz-import plugin
705
                from_branch = pybaz.Version(from_branch)
83 by Aaron Bentley
Moved most baz2bzr code to baz_import, added Python plugin
706
            except pybaz.errors.NamespaceError:
707
                print "%s is not a valid Arch branch." % from_branch
708
                return 1
147.4.13 by Robert Collins
add a reuse_history option to the baz-import-branch command too.
709
        if reuse_history_list is None:
710
            reuse_history_list = []
147.2.10 by Aaron Bentley
Improved error handling, esp when invoking on wrong branch
711
        import_version(to_location, from_branch, self.printer, 
147.1.44 by Aaron Bentley
Merged the bzrtools mainline
712
                       max_count=max_count, 
713
                       reuse_history_from=reuse_history_list)
714
715
716
class NotInABranch(Exception):
717
    def __init__(self, path):
718
        Exception.__init__(self, "%s is not in a branch." % path)
719
        self.path = path
147.1.11 by Robert Collins
move baz-import to baz-import-branch
720
147.1.29 by Robert Collins
update to latest bzr api
721
147.1.11 by Robert Collins
move baz-import to baz-import-branch
722
class cmd_baz_import(Command):
147.4.8 by Robert Collins
Enable reuse of history on archive imports, just append the history locations to the command line
723
    """Import an Arch or Baz archive into bzr branches.
724
    
725
    reuse_history allows you to specify any previous imports you 
726
    have done of different archives, which this archive has branches
727
    tagged from. This will dramatically reduce the time to convert 
728
    the archive as it will not have to convert the history already
729
    converted in that other branch.
730
    """
731
    takes_args = ['to_root_dir', 'from_archive', 'reuse_history*']
147.1.11 by Robert Collins
move baz-import to baz-import-branch
732
    takes_options = ['verbose']
733
147.1.16 by Robert Collins
make feedback be callback based
734
    def printer(self, name):
735
        print name
736
147.4.8 by Robert Collins
Enable reuse of history on archive imports, just append the history locations to the command line
737
    def run(self, to_root_dir, from_archive, verbose=False,
738
            reuse_history_list=[]):
147.4.9 by Robert Collins
But do not require a history location.
739
        if reuse_history_list is None:
740
            reuse_history_list = []
147.1.18 by Robert Collins
baz-import twice should work
741
        to_root = str(os.path.realpath(to_root_dir))
147.1.12 by Robert Collins
create the output directory
742
        if not os.path.exists(to_root):
743
            os.mkdir(to_root)
147.4.8 by Robert Collins
Enable reuse of history on archive imports, just append the history locations to the command line
744
        import_archive(to_root, from_archive, verbose, self.printer, 
745
                       reuse_history_list)
746
747
748
def import_archive(to_root, from_archive, verbose, printer,
749
                   reuse_history_from=[]):
750
    real_to = os.path.realpath(to_root)
751
    history_locations = [real_to] + reuse_history_from
147.1.18 by Robert Collins
baz-import twice should work
752
    for version in pybaz.Archive(str(from_archive)).iter_versions():
147.1.15 by Robert Collins
archive at a time imports
753
        target = os.path.join(to_root, map_namespace(version))
147.1.16 by Robert Collins
make feedback be callback based
754
        printer("importing %s into %s" % (version, target))
147.1.18 by Robert Collins
baz-import twice should work
755
        if not os.path.exists(os.path.dirname(target)):
756
            os.makedirs(os.path.dirname(target))
147.4.3 by Robert Collins
dont block archive wide baz-imports when a single version fails dur to either an unbuildable revision or a diverged branch.
757
        try:
147.4.8 by Robert Collins
Enable reuse of history on archive imports, just append the history locations to the command line
758
            import_version(target, version, printer,
759
                           reuse_history_from=reuse_history_from)
147.4.3 by Robert Collins
dont block archive wide baz-imports when a single version fails dur to either an unbuildable revision or a diverged branch.
760
        except pybaz.errors.ExecProblem,e:
761
            if str(e).find('The requested revision cannot be built.') != -1:
762
                printer("Skipping version %s as it cannot be built due"
763
                        " to a missing parent archive." % version)
764
            else:
765
                raise
766
        except UserError, e:
767
            if str(e).find('already exists, and the last revision ') != -1:
768
                printer("Skipping version %s as it has had commits made"
769
                        " since it was converted to bzr." % version)
770
            else:
771
                raise
147.1.13 by Robert Collins
create the output directory
772
147.1.29 by Robert Collins
update to latest bzr api
773
147.1.13 by Robert Collins
create the output directory
774
def map_namespace(a_version):
147.1.14 by Robert Collins
implement a namespace mapper
775
    a_version = pybaz.Version("%s" % a_version)
776
    parser = NameParser(a_version)
777
    version = parser.get_version()
778
    branch = parser.get_branch()
779
    category = parser.get_category()
780
    if branch is None or branch == '':
781
        branch = "+trunk"
782
    if version == '0':
783
        return "%s/%s" % (category, branch)
784
    return "%s/%s/%s" % (category, version, branch)
147.4.1 by Robert Collins
test escaping of file ids is working
785
786
def map_file_id(file_id):
787
    """Convert a baz file id to a bzr one."""
788
    return file_id.replace('%', '%25').replace('/', '%2f')