1
# Copyright (C) 2005 by Aaron Bentley
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.
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.
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
19
from bzrlib.errors import BzrError
20
from bzrlib.errors import NotBranchError, BzrCommandError, NoSuchRevision
21
from bzrlib.branch import Branch
22
from bzrlib.clone import copy_branch
23
from bzrlib.commit import Commit, NullCommitReporter
24
from bzrlib.commands import Command
25
from bzrlib.option import _global_option
26
from bzrlib.workingtree import WorkingTree
27
from errors import NoPyBaz
31
from pybaz import NameParser as NameParser
32
from pybaz.backends.baz import null_cmd
35
from fai import iter_new_merges, direct_merges
43
import bzrlib.inventory
47
from progress import *
49
class ImportCommitReporter(NullCommitReporter):
50
def __init__(self, pb):
53
def escaped(self, escape_count, message):
55
bzrlib.trace.warning("replaced %d control characters in message" %
58
def add_id(files, id=None):
59
"""Adds an explicit id to a list of files.
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
68
args.extend(["--id", id])
76
>>> q = test_environ()
79
>>> os.path.exists(os.path.join(q, "home", ".arch-params"))
81
>>> teardown_environ(q)
86
saved_dir = os.getcwdu()
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")
95
pybaz.init_tree(work_dir, "test@example.com/test--test--0")
96
lib_dir = os.path.join(tdir, "lib_dir")
98
pybaz.register_revision_library(lib_dir)
99
pybaz.set_my_id("Test User<test@example.org>")
102
def add_file(path, text, id):
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
110
>>> teardown_environ(q)
112
file(path, "wb").write(text)
116
def add_dir(path, id):
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
124
>>> teardown_environ(q)
129
def teardown_environ(tdir):
133
def timport(tree, summary):
134
msg = tree.log_message()
135
msg["summary"] = summary
138
def commit(tree, summary):
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()]
148
'test@example.com/test--test--0--base-0'
150
'test@example.com/test--test--0--patch-1'
151
>>> teardown_environ(q)
153
msg = tree.log_message()
154
msg["summary"] = summary
157
def commit_test_revisions():
159
>>> q = test_environ()
160
>>> commit_test_revisions()
161
>>> a = pybaz.Archive("test@example.com")
162
>>> revisions = list(a.iter_revisions("test--test--0"))
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)
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")
182
def commit_more_test_revisions():
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"))
191
>>> str(revisions[0])
192
'test@example.com/test--test--0--patch-3'
193
>>> teardown_environ(q)
195
tree = pybaz.tree_root(".")
196
add_file("trainfile", "void train(void){}", "trainfile by aaron")
197
commit(tree, "altered trainfile")
199
class NoSuchVersion(Exception):
200
def __init__(self, version):
201
Exception.__init__(self, "The version %s does not exist." % version)
202
self.version = version
204
def version_ancestry(version):
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)
221
revision = version.iter_revisions(reverse=True).next()
222
except StopIteration:
226
if not version.exists():
227
raise NoSuchVersion(version)
230
ancestors = list(revision.iter_ancestors(metoo=True))
234
def get_last_revision(branch):
235
last_patch = branch.last_revision()
237
return arch_revision(last_patch)
238
except NotArchRevision:
240
"Directory \"%s\" already exists, and the last revision is not"
241
" an Arch revision (%s)" % (branch.base, last_patch))
243
def do_branch(br_from, to_location, revision_id):
244
"""Derived from branch in builtins."""
248
os.mkdir(to_location)
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.' %
259
copy_branch(br_from, to_location, revision_id, None)
260
except NoSuchRevision:
262
msg = "The branch %s has no revision %s." % (from_location, revision_id)
267
def get_remaining_revisions(output_dir, version, reuse_history_from=[]):
270
output_exists = os.path.exists(output_dir)
272
# We are starting from an existing directory, figure out what
273
# the current version is
274
branch = Branch.open(output_dir)
275
last_patch = get_last_revision(branch)
277
version = last_patch.version
278
elif version is None:
279
raise UserError("No version specified, and directory does not exist.")
282
ancestors = version_ancestry(version)
283
if not output_exists and reuse_history_from != []:
284
for ancestor in reversed(ancestors):
285
if last_patch is not None:
286
# found something to copy
288
# try to grab a copy of ancestor
289
# note that is not optimised: we could look for namespace
290
# transitions and only look for the past after the
292
for history_root in reuse_history_from:
293
possible_source = os.path.join(history_root,
294
map_namespace(ancestor.version))
296
source = Branch.open(possible_source)
297
rev_id = revision_id(ancestor)
298
if rev_id in source.revision_history():
299
do_branch(source, output_dir, rev_id)
300
last_patch = ancestor
302
except NotBranchError:
304
except NoSuchVersion, e:
305
raise UserError(str(e))
308
for i in range(len(ancestors)):
309
if ancestors[i] == last_patch:
312
raise UserError("Directory \"%s\" already exists, and the last "
313
"revision (%s) is not in the ancestry of %s" %
314
(output_dir, last_patch, version))
315
# Strip off all of the ancestors which are already present
316
# And get a directory starting with the latest ancestor
317
latest_ancestor = ancestors[i]
318
old_revno = Branch.open(output_dir).revno()
319
ancestors = ancestors[i+1:]
320
return ancestors, old_revno
323
###class Importer(object):
326
### Currently this is used as a parameter object, though more behaviour is
330
### def __init__(self, output_dir, version, printer, fancy=True, fast=False,
331
### verbose=False, dry_run=False, max_count=None,
332
### reuse_history_from=[]):
333
### self.output_dir = output_dir
334
### self.version = version
338
def import_version(output_dir, version, printer, fancy=True, fast=False,
339
verbose=False, dry_run=False, max_count=None,
340
reuse_history_from=[]):
342
>>> q = test_environ()
343
>>> result_path = os.path.join(q, "result")
344
>>> commit_test_revisions()
345
>>> version = pybaz.Version("test@example.com/test--test--0.1")
346
>>> def printer(message): print message
347
>>> import_version('/', version, printer, fancy=False, dry_run=True)
348
Traceback (most recent call last):
349
UserError: / exists, but is not a bzr branch.
350
>>> import_version(result_path, version, printer, fancy=False, dry_run=True)
351
Traceback (most recent call last):
352
UserError: The version test@example.com/test--test--0.1 does not exist.
353
>>> version = pybaz.Version("test@example.com/test--test--0")
354
>>> import_version(result_path, version, printer, fancy=False, dry_run=True)
357
Dry run, not modifying output_dir
359
>>> import_version(result_path, version, printer, fancy=False)
364
>>> import_version(result_path, version, printer, fancy=False)
365
Tree is up-to-date with test@example.com/test--test--0--patch-2
366
>>> commit_more_test_revisions()
367
>>> import_version(result_path, version, printer, fancy=False)
372
>>> teardown_environ(q)
375
ancestors, old_revno = get_remaining_revisions(output_dir, version,
377
except NotBranchError, e:
378
raise UserError("%s exists, but is not a bzr branch." % output_dir)
379
if old_revno is None and len(ancestors) == 0:
380
print 'Version %s has no revisions.' % version
382
if len(ancestors) == 0:
383
last_revision = get_last_revision(Branch.open(output_dir))
384
print 'Tree is up-to-date with %s' % last_revision
387
progress_bar = ProgressBar()
388
tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
389
dir=os.path.dirname(output_dir))
394
for result in iter_import_version(output_dir, ancestors, tempdir,
395
progress_bar, fast=fast, verbose=verbose, dry_run=dry_run,
396
max_count=max_count):
398
show_progress(progress_bar, result)
400
sys.stdout.write('.')
405
sys.stdout.write('\n')
408
print 'Dry run, not modifying output_dir'
410
if os.path.exists(output_dir):
411
# Move the bzr control directory back, and update the working tree
412
revdir = os.path.join(tempdir, "rd")
413
if os.path.exists(revdir):
414
# actual imports were done
415
tmp_bzr_dir = os.path.join(tempdir, '.bzr')
417
bzr_dir = os.path.join(output_dir, '.bzr')
418
new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
420
os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
421
os.rename(new_bzr_dir, bzr_dir)
423
bzrlib.merge.merge((output_dir, -1), (output_dir, None), # old_revno),
424
check_clean=False, this_dir=output_dir,
427
# If something failed, move back the original bzr directory
428
os.rename(bzr_dir, new_bzr_dir)
429
os.rename(tmp_bzr_dir, bzr_dir)
432
# no imports - perhaps just append_revisions
434
bzrlib.merge.merge((output_dir, -1), (output_dir, None), # old_revno),
435
check_clean=False, this_dir=output_dir,
438
revdir = os.path.join(tempdir, "rd")
439
os.rename(revdir, output_dir)
442
printer('Cleaning up')
443
shutil.rmtree(tempdir)
444
printer("Import complete.")
446
class UserError(BzrCommandError):
447
def __init__(self, message):
448
"""Exception to throw when a user makes an impossible request
449
:param message: The message to emit when printing this exception
450
:type message: string
452
BzrCommandError.__init__(self, message)
454
def revision_id(arch_revision):
456
Generate a Bzr revision id from an Arch revision id. 'x' in the id
457
designates a revision imported with an experimental algorithm. A number
458
would indicate a particular standardized version.
460
:param arch_revision: The Arch revision to generate an ID for.
462
>>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
463
'Arch-1:you@example.com%cat--br--0--base-0'
465
return "Arch-1:%s" % str(arch_revision).replace('/', '%')
467
class NotArchRevision(Exception):
468
def __init__(self, revision_id):
469
msg = "The revision id %s does not look like it came from Arch."\
471
Exception.__init__(self, msg)
473
def arch_revision(revision_id):
475
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0"))
476
Traceback (most recent call last):
477
NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0 does not look like it came from Arch.
478
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--base-5"))
479
Traceback (most recent call last):
480
NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
481
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--patch-5"))
482
'jrandom@example.com/test--test--0--patch-5'
484
if revision_id is None:
486
if revision_id[:7] != 'Arch-1:':
487
raise NotArchRevision(revision_id)
490
return pybaz.Revision(revision_id[7:].replace('%', '/'))
491
except pybaz.errors.NamespaceError, e:
492
raise NotArchRevision(revision_id)
494
def iter_import_version(output_dir, ancestors, tempdir, pb, fast=False,
495
verbose=False, dry_run=False, max_count=None):
498
# Uncomment this for testing, it basically just has baz2bzr only update
499
# 5 patches at a time
501
ancestors = ancestors[:max_count]
503
# Not sure if I want this output. basically it tells you ahead of time
504
# what it is going to do, but then later it tells you as it is doing it.
505
# what probably would be best would be to collapse it into ranges, so that
506
# this gives the simple view, and then later it gives the blow by blow.
508
# print 'Adding the following revisions:'
509
# for a in ancestors:
512
previous_version=None
513
missing_ancestor = None
515
for i in range(len(ancestors)):
516
revision = ancestors[i]
517
rev_id = revision_id(revision)
520
version = str(revision.version)
521
if version != previous_version:
523
print '\rOn version: %s' % version
524
yield Progress(str(revision.patchlevel), i, len(ancestors))
525
previous_version = version
527
yield Progress("revisions", i, len(ancestors))
528
if revdir is None and os.path.exists(output_dir):
529
# check for imported revisions and if present just append immediately
530
branch = Branch.open(output_dir)
531
if branch.has_revision(rev_id):
532
branch.append_revision(rev_id)
535
revdir = os.path.join(tempdir, "rd")
537
tree, baz_inv, log = get_revision(revdir, revision)
538
except pybaz.errors.ExecProblem, e:
539
if ("%s" % e.args).find('could not connect') == -1:
541
missing_ancestor = revision
543
print ("unable to access ancestor %s, making into a merge."
546
if os.path.exists(output_dir):
547
bzr_dir = os.path.join(output_dir, '.bzr')
548
new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
549
# This would be much faster with a simple os.rename(), but if
550
# we fail, we have corrupted the original .bzr directory. Is
551
# that a big problem, as we can just back out the last
552
# revisions in .bzr/revision_history I don't really know
553
# RBC20051024 - yes, it would be a problem as we could not then
554
# apply the corrupted revision.
555
shutil.copytree(bzr_dir, new_bzr_dir)
556
# Now revdir should have a tree with the latest .bzr, and the
557
# next revision of the baz tree
558
branch = Branch.open(revdir)
560
branch = Branch.initialize(revdir)
562
old = os.path.join(revdir, ".bzr")
563
new = os.path.join(tempdir, ".bzr")
565
baz_inv, log = apply_revision(tree, revision)
567
branch = Branch.open(revdir)
568
# cached so we can delete the log
570
log_summary = log.summary
571
log_description = log.description
572
is_continuation = log.continuation_of is not None
573
log_creator = log.creator
574
direct_merges = get_direct_merges(revdir, revision, log)
576
timestamp = email.Utils.mktime_tz(log_date + (0,))
577
if log_summary is None:
579
# log_descriptions of None and "" are ignored.
580
if not is_continuation and log_description:
581
log_message = "\n".join((log_summary, log_description))
583
log_message = log_summary
585
target_tree = WorkingTree(revdir ,branch=branch)
586
target_tree.lock_write()
589
# if we want it to be in revision-history, do that here.
590
branch.add_pending_merge(revision_id(missing_ancestor))
591
missing_ancestor = None
592
for merged_rev in direct_merges:
593
branch.add_pending_merge(revision_id(merged_rev))
594
target_tree.set_inventory(baz_inv)
595
commitobj = Commit(reporter=ImportCommitReporter(pb))
596
commitobj.commit(branch, log_message.decode('ascii', 'replace'),
597
verbose=False, committer=log_creator,
598
timestamp=timestamp, timezone=0, rev_id=rev_id)
602
yield Progress("revisions", len(ancestors), len(ancestors))
603
unlink_unversioned(branch, revdir)
605
def get_direct_merges(revdir, revision, log):
606
continuation = log.continuation_of
607
previous_version = revision.version
608
if pybaz.WorkingTree(revdir).tree_version != previous_version:
609
pybaz.WorkingTree(revdir).set_tree_version(previous_version)
610
log_path = "%s/{arch}/%s/%s/%s/%s/patch-log/%s" % (revdir,
611
revision.category.nonarch, revision.branch.nonarch,
612
revision.version.nonarch, revision.archive, revision.patchlevel)
613
temp_path = tempfile.mktemp(dir=os.path.dirname(revdir))
614
os.rename(log_path, temp_path)
615
merges = list(iter_new_merges(revdir, revision.version))
616
direct = direct_merges(merges, [continuation])
617
os.rename(temp_path, log_path)
620
def unlink_unversioned(branch, revdir):
621
for unversioned in branch.working_tree().extras():
622
path = os.path.join(revdir, unversioned)
623
if os.path.isdir(path):
628
def get_log(tree, revision):
629
log = pybaz.Patchlog(revision, tree=tree)
630
assert str(log.revision) == str(revision), (log.revision, revision)
633
def get_revision(revdir, revision):
634
tree = revision.get(revdir)
635
log = get_log(tree, revision)
637
return tree, bzr_inventory_data(tree), log
638
except BadFileKind, e:
639
raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
642
def apply_revision(tree, revision):
644
log = get_log(tree, revision)
646
return bzr_inventory_data(tree), log
647
except BadFileKind, e:
648
raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
651
class BadFileKind(Exception):
652
"""The file kind is not permitted in bzr inventories"""
653
def __init__(self, tree_root, path, kind):
654
self.tree_root = tree_root
657
Exception.__init__(self, "File %s is of forbidden type %s" %
658
(os.path.join(tree_root, path), kind))
661
def bzr_inventory_data(tree):
662
inv_iter = tree.iter_inventory_ids(source=True, both=True)
664
for arch_id, path in inv_iter:
665
bzr_file_id = map_file_id(arch_id)
666
inv_map[path] = bzr_file_id
669
for path, file_id in inv_map.iteritems():
670
full_path = os.path.join(tree, path)
671
kind = bzrlib.osutils.file_kind(full_path)
672
if kind not in ("file", "directory", "symlink"):
673
raise BadFileKind(tree, path, kind)
674
parent_dir = os.path.dirname(path)
676
parent_id = inv_map[parent_dir]
678
parent_id = bzrlib.inventory.ROOT_ID
679
bzr_inv.append((path, file_id, parent_id, kind))
683
_global_option('max-count', type = int)
684
class cmd_baz_import_branch(Command):
685
"""Import an Arch or Baz branch into a bzr branch"""
686
takes_args = ['to_location', 'from_branch?', 'reuse_history*']
687
takes_options = ['verbose', 'max-count']
689
def printer(self, name):
692
def run(self, to_location, from_branch=None, fast=False, max_count=None,
693
verbose=False, dry_run=False, reuse_history_list=[]):
694
to_location = os.path.realpath(str(to_location))
695
if from_branch is not None:
697
from_branch = pybaz.Version(from_branch)
698
except pybaz.errors.NamespaceError:
699
print "%s is not a valid Arch branch." % from_branch
701
if reuse_history_list is None:
702
reuse_history_list = []
703
import_version(to_location, from_branch, self.printer,
704
max_count=max_count, reuse_history_from=reuse_history_list)
707
class cmd_baz_import(Command):
708
"""Import an Arch or Baz archive into bzr branches.
710
reuse_history allows you to specify any previous imports you
711
have done of different archives, which this archive has branches
712
tagged from. This will dramatically reduce the time to convert
713
the archive as it will not have to convert the history already
714
converted in that other branch.
716
takes_args = ['to_root_dir', 'from_archive', 'reuse_history*']
717
takes_options = ['verbose']
719
def printer(self, name):
722
def run(self, to_root_dir, from_archive, verbose=False,
723
reuse_history_list=[]):
724
if reuse_history_list is None:
725
reuse_history_list = []
726
to_root = str(os.path.realpath(to_root_dir))
727
if not os.path.exists(to_root):
729
import_archive(to_root, from_archive, verbose, self.printer,
733
def import_archive(to_root, from_archive, verbose, printer,
734
reuse_history_from=[]):
735
real_to = os.path.realpath(to_root)
736
history_locations = [real_to] + reuse_history_from
737
for version in pybaz.Archive(str(from_archive)).iter_versions():
738
target = os.path.join(to_root, map_namespace(version))
739
printer("importing %s into %s" % (version, target))
740
if not os.path.exists(os.path.dirname(target)):
741
os.makedirs(os.path.dirname(target))
743
import_version(target, version, printer,
744
reuse_history_from=reuse_history_from)
745
except pybaz.errors.ExecProblem,e:
746
if str(e).find('The requested revision cannot be built.') != -1:
747
printer("Skipping version %s as it cannot be built due"
748
" to a missing parent archive." % version)
752
if str(e).find('already exists, and the last revision ') != -1:
753
printer("Skipping version %s as it has had commits made"
754
" since it was converted to bzr." % version)
759
def map_namespace(a_version):
760
a_version = pybaz.Version("%s" % a_version)
761
parser = NameParser(a_version)
762
version = parser.get_version()
763
branch = parser.get_branch()
764
category = parser.get_category()
765
if branch is None or branch == '':
768
return "%s/%s" % (category, branch)
769
return "%s/%s/%s" % (category, version, branch)
771
def map_file_id(file_id):
772
"""Convert a baz file id to a bzr one."""
773
return file_id.replace('%', '%25').replace('/', '%2f')