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
17
from bzrlib.errors import NotBranchError
18
from bzrlib.branch import Branch
19
from bzrlib.commit import Commit, NullCommitReporter
20
from bzrlib.commands import Command
21
from errors import NoPyBaz
25
from pybaz import NameParser as NameParser
26
from pybaz.backends.baz import null_cmd
29
from fai import iter_new_merges, direct_merges
35
from bzrlib.errors import BzrError
38
import bzrlib.inventory
42
from progress import *
44
class ImportCommitReporter(NullCommitReporter):
45
def __init__(self, pb):
48
def escaped(self, escape_count, message):
50
bzrlib.trace.warning("replaced %d control characters in message" %
53
def add_id(files, id=None):
54
"""Adds an explicit id to a list of files.
56
:param files: the name of the file to add an id to
57
:type files: list of str
58
:param id: tag one file using the specified id, instead of generating id
63
args.extend(["--id", id])
71
>>> q = test_environ()
74
>>> os.path.exists(os.path.join(q, "home", ".arch-params"))
76
>>> teardown_environ(q)
81
saved_dir = os.getcwdu()
82
tdir = tempfile.mkdtemp(prefix="testdir-")
83
os.environ["HOME"] = os.path.join(tdir, "home")
84
os.mkdir(os.environ["HOME"])
85
arch_dir = os.path.join(tdir, "archive_dir")
86
pybaz.make_archive("test@example.com", arch_dir)
87
work_dir = os.path.join(tdir, "work_dir")
90
pybaz.init_tree(work_dir, "test@example.com/test--test--0")
91
lib_dir = os.path.join(tdir, "lib_dir")
93
pybaz.register_revision_library(lib_dir)
94
pybaz.set_my_id("Test User<test@example.org>")
97
def add_file(path, text, id):
99
>>> q = test_environ()
100
>>> add_file("path with space", "text", "lalala")
101
>>> tree = pybaz.tree_root(".")
102
>>> inv = list(tree.iter_inventory_ids(source=True, both=True))
103
>>> ("x_lalala", "path with space") in inv
105
>>> teardown_environ(q)
107
file(path, "wb").write(text)
111
def add_dir(path, id):
113
>>> q = test_environ()
114
>>> add_dir("path with\(sp) space", "lalala")
115
>>> tree = pybaz.tree_root(".")
116
>>> inv = list(tree.iter_inventory_ids(source=True, both=True))
117
>>> ("x_lalala", "path with\(sp) space") in inv
119
>>> teardown_environ(q)
124
def teardown_environ(tdir):
128
def timport(tree, summary):
129
msg = tree.log_message()
130
msg["summary"] = summary
133
def commit(tree, summary):
135
>>> q = test_environ()
136
>>> tree = pybaz.tree_root(".")
137
>>> timport(tree, "import")
138
>>> commit(tree, "commit")
139
>>> logs = [str(l.revision) for l in tree.iter_logs()]
143
'test@example.com/test--test--0--base-0'
145
'test@example.com/test--test--0--patch-1'
146
>>> teardown_environ(q)
148
msg = tree.log_message()
149
msg["summary"] = summary
152
def commit_test_revisions():
154
>>> q = test_environ()
155
>>> commit_test_revisions()
156
>>> a = pybaz.Archive("test@example.com")
157
>>> revisions = list(a.iter_revisions("test--test--0"))
160
>>> str(revisions[2])
161
'test@example.com/test--test--0--base-0'
162
>>> str(revisions[1])
163
'test@example.com/test--test--0--patch-1'
164
>>> str(revisions[0])
165
'test@example.com/test--test--0--patch-2'
166
>>> teardown_environ(q)
168
tree = pybaz.tree_root(".")
169
add_file("mainfile", "void main(void){}", "mainfile by aaron")
170
timport(tree, "Created mainfile")
171
file("mainfile", "wb").write("or something like that")
172
commit(tree, "altered mainfile")
173
add_file("ofile", "this is another file", "ofile by aaron")
174
commit(tree, "altered mainfile")
177
def commit_more_test_revisions():
179
>>> q = test_environ()
180
>>> commit_test_revisions()
181
>>> commit_more_test_revisions()
182
>>> a = pybaz.Archive("test@example.com")
183
>>> revisions = list(a.iter_revisions("test--test--0"))
186
>>> str(revisions[0])
187
'test@example.com/test--test--0--patch-3'
188
>>> teardown_environ(q)
190
tree = pybaz.tree_root(".")
191
add_file("trainfile", "void train(void){}", "trainfile by aaron")
192
commit(tree, "altered trainfile")
194
class NoSuchVersion(Exception):
195
def __init__(self, version):
196
Exception.__init__(self, "The version %s does not exist." % version)
197
self.version = version
199
def version_ancestry(version):
201
>>> q = test_environ()
202
>>> commit_test_revisions()
203
>>> version = pybaz.Version("test@example.com/test--test--0")
204
>>> ancestors = version_ancestry(version)
205
>>> str(ancestors[0])
206
'test@example.com/test--test--0--base-0'
207
>>> str(ancestors[1])
208
'test@example.com/test--test--0--patch-1'
209
>>> version = pybaz.Version("test@example.com/test--test--0.5")
210
>>> ancestors = version_ancestry(version)
211
Traceback (most recent call last):
212
NoSuchVersion: The version test@example.com/test--test--0.5 does not exist.
213
>>> teardown_environ(q)
216
revision = version.iter_revisions(reverse=True).next()
217
except StopIteration:
221
if not version.exists():
222
raise NoSuchVersion(version)
225
ancestors = list(revision.iter_ancestors(metoo=True))
229
def get_last_revision(branch):
230
last_patch = branch.last_revision()
232
return arch_revision(last_patch)
233
except NotArchRevision:
235
"Directory \"%s\" already exists, and the last revision is not"
236
" an Arch revision (%s)" % (output_dir, last_patch))
239
def get_remaining_revisions(output_dir, version):
242
if os.path.exists(output_dir):
243
# We are starting from an existing directory, figure out what
244
# the current version is
245
branch = Branch.open(output_dir)
246
last_patch = get_last_revision(branch)
248
version = last_patch.version
249
elif version is None:
250
raise UserError("No version specified, and directory does not exist.")
253
ancestors = version_ancestry(version)
254
except NoSuchVersion, e:
258
for i in range(len(ancestors)):
259
if ancestors[i] == last_patch:
262
raise UserError("Directory \"%s\" already exists, and the last "
263
"revision (%s) is not in the ancestry of %s" %
264
(output_dir, last_patch, version))
265
# Strip off all of the ancestors which are already present
266
# And get a directory starting with the latest ancestor
267
latest_ancestor = ancestors[i]
268
old_revno = Branch.open(output_dir).revno()
269
ancestors = ancestors[i+1:]
270
return ancestors, old_revno
272
def import_version(output_dir, version, printer, fancy=True, fast=False,
273
verbose=False, dry_run=False, max_count=None):
275
>>> q = test_environ()
276
>>> result_path = os.path.join(q, "result")
277
>>> commit_test_revisions()
278
>>> version = pybaz.Version("test@example.com/test--test--0.1")
279
>>> def printer(message): print message
280
>>> import_version('/', version, printer, fancy=False, dry_run=True)
281
Traceback (most recent call last):
282
UserError: / exists, but is not a bzr branch.
283
>>> import_version(result_path, version, printer, fancy=False, dry_run=True)
284
Traceback (most recent call last):
285
UserError: The version test@example.com/test--test--0.1 does not exist.
286
>>> version = pybaz.Version("test@example.com/test--test--0")
287
>>> import_version(result_path, version, printer, fancy=False, dry_run=True)
290
Dry run, not modifying output_dir
292
>>> import_version(result_path, version, printer, fancy=False)
297
>>> import_version(result_path, version, printer, fancy=False)
298
Tree is up-to-date with test@example.com/test--test--0--patch-2
299
>>> commit_more_test_revisions()
300
>>> import_version(result_path, version, printer, fancy=False)
305
>>> teardown_environ(q)
308
ancestors, old_revno = get_remaining_revisions(output_dir, version)
309
except NotBranchError, e:
310
raise UserError("%s exists, but is not a bzr branch." % output_dir)
311
if old_revno is None and len(ancestors) == 0:
312
print 'Version %s has no revisions.' % version
314
if len(ancestors) == 0:
315
last_revision = get_last_revision(Branch.open(output_dir))
316
print 'Tree is up-to-date with %s' % last_revision
319
progress_bar = ProgressBar()
320
tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
321
dir=os.path.dirname(output_dir))
326
for result in iter_import_version(output_dir, ancestors, tempdir,
327
progress_bar, fast=fast, verbose=verbose, dry_run=dry_run,
328
max_count=max_count):
330
show_progress(progress_bar, result)
332
sys.stdout.write('.')
337
sys.stdout.write('\n')
340
print 'Dry run, not modifying output_dir'
342
if os.path.exists(output_dir):
343
# Move the bzr control directory back, and update the working tree
344
tmp_bzr_dir = os.path.join(tempdir, '.bzr')
346
bzr_dir = os.path.join(output_dir, '.bzr')
347
new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
349
os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
350
os.rename(new_bzr_dir, bzr_dir)
352
bzrlib.merge.merge((output_dir, -1), (output_dir, None), # old_revno),
353
check_clean=False, this_dir=output_dir,
356
# If something failed, move back the original bzr directory
357
os.rename(bzr_dir, new_bzr_dir)
358
os.rename(tmp_bzr_dir, bzr_dir)
361
revdir = os.path.join(tempdir, "rd")
362
os.rename(revdir, output_dir)
365
printer('Cleaning up')
366
shutil.rmtree(tempdir)
367
printer("Import complete.")
369
class UserError(Exception):
370
def __init__(self, message):
371
"""Exception to throw when a user makes an impossible request
372
:param message: The message to emit when printing this exception
373
:type message: string
375
Exception.__init__(self, message)
377
def revision_id(arch_revision):
379
Generate a Bzr revision id from an Arch revision id. 'x' in the id
380
designates a revision imported with an experimental algorithm. A number
381
would indicate a particular standardized version.
383
:param arch_revision: The Arch revision to generate an ID for.
385
>>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
386
'Arch-1:you@example.com%cat--br--0--base-0'
388
return "Arch-1:%s" % str(arch_revision).replace('/', '%')
390
class NotArchRevision(Exception):
391
def __init__(self, revision_id):
392
msg = "The revision id %s does not look like it came from Arch."\
394
Exception.__init__(self, msg)
396
def arch_revision(revision_id):
398
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0"))
399
Traceback (most recent call last):
400
NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0 does not look like it came from Arch.
401
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--base-5"))
402
Traceback (most recent call last):
403
NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
404
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--patch-5"))
405
'jrandom@example.com/test--test--0--patch-5'
407
if revision_id is None:
409
if revision_id[:7] != 'Arch-1:':
410
raise NotArchRevision(revision_id)
413
return pybaz.Revision(revision_id[7:].replace('%', '/'))
414
except pybaz.errors.NamespaceError, e:
415
raise NotArchRevision(revision_id)
417
def iter_import_version(output_dir, ancestors, tempdir, pb, fast=False,
418
verbose=False, dry_run=False, max_count=None):
421
# Uncomment this for testing, it basically just has baz2bzr only update
422
# 5 patches at a time
424
ancestors = ancestors[:max_count]
426
# Not sure if I want this output. basically it tells you ahead of time
427
# what it is going to do, but then later it tells you as it is doing it.
428
# what probably would be best would be to collapse it into ranges, so that
429
# this gives the simple view, and then later it gives the blow by blow.
431
# print 'Adding the following revisions:'
432
# for a in ancestors:
435
previous_version=None
436
missing_ancestor = None
438
for i in range(len(ancestors)):
439
revision = ancestors[i]
442
version = str(revision.version)
443
if version != previous_version:
445
print '\rOn version: %s' % version
446
yield Progress(str(revision.patchlevel), i, len(ancestors))
447
previous_version = version
449
yield Progress("revisions", i, len(ancestors))
451
revdir = os.path.join(tempdir, "rd")
453
baz_inv, log = get_revision(revdir, revision)
454
except pybaz.errors.ExecProblem, e:
455
if ("%s" % e.args).find('could not connect') == -1:
457
missing_ancestor = revision
459
print ("unable to access ancestor %s, making into a merge."
462
# cached so we can delete the log
464
log_summary = log.summary
465
log_creator = log.creator
466
if os.path.exists(output_dir):
467
bzr_dir = os.path.join(output_dir, '.bzr')
468
new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
469
# This would be much faster with a simple os.rename(), but if
470
# we fail, we have corrupted the original .bzr directory. Is
471
# that a big problem, as we can just back out the last
472
# revisions in .bzr/revision_history I don't really know
473
shutil.copytree(bzr_dir, new_bzr_dir)
474
# Now revdir should have a tree with the latest .bzr, and the
475
# next revision of the baz tree
476
branch = Branch.open(revdir)
478
branch = Branch.initialize(revdir)
480
old = os.path.join(revdir, ".bzr")
481
new = os.path.join(tempdir, ".bzr")
483
baz_inv, log = apply_revision(revdir, revision)
485
log_summary = log.summary
486
log_creator = log.creator
487
direct_merges = get_direct_merges(revdir, revision)
489
branch = Branch.open(revdir)
490
timestamp = email.Utils.mktime_tz(log_date + (0,))
491
rev_id = revision_id(revision)
492
if log_summary is None:
497
# if we want it to be in revision-history, do that here.
498
branch.add_pending_merge(revision_id(missing_ancestor))
499
missing_ancestor = None
500
for merge in direct_merges:
501
branch.add_pending_merge(revision_id(merge.revision))
502
branch.set_inventory(baz_inv)
503
commitobj = Commit(reporter=ImportCommitReporter(pb))
504
commitobj.commit(branch, log_summary, verbose=False,
505
committer=log_creator, timestamp=timestamp,
506
timezone=0, rev_id=rev_id)
509
yield Progress("revisions", len(ancestors), len(ancestors))
510
unlink_unversioned(branch, revdir)
512
def get_direct_merges(revdir, revision):
513
if pybaz.WorkingTree(revdir).tree_version != revision.version:
514
pybaz.WorkingTree(revdir).set_tree_version(revision.version)
515
log_path = "%s/{arch}/%s/%s/%s/%s/patch-log/%s" % (revdir,
516
revision.category.nonarch, revision.branch.nonarch,
517
revision.version.nonarch, revision.archive, revision.patchlevel)
518
temp_path = tempfile.mktemp()
519
os.rename(log_path, temp_path)
520
merges = list(iter_new_merges(revdir, revision.version))
521
direct = direct_merges (merges)
522
os.rename(temp_path, log_path)
525
def unlink_unversioned(branch, revdir):
526
for unversioned in branch.working_tree().extras():
527
path = os.path.join(revdir, unversioned)
528
if os.path.isdir(path):
533
def get_log(tree, revision):
534
log = tree.iter_logs(version=revision.version, reverse=True).next()
535
assert str(log.revision) == str(revision), (log.revision, revision)
538
def get_revision(revdir, revision):
540
tree = pybaz.tree_root(revdir)
541
log = get_log(tree, revision)
543
return bzr_inventory_data(tree), log
544
except BadFileKind, e:
545
raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
548
def apply_revision(revdir, revision):
549
tree = pybaz.tree_root(revdir)
551
log = get_log(tree, revision)
553
return bzr_inventory_data(tree), log
554
except BadFileKind, e:
555
raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
558
class BadFileKind(Exception):
559
"""The file kind is not permitted in bzr inventories"""
560
def __init__(self, tree_root, path, kind):
561
self.tree_root = tree_root
564
Exception.__init__(self, "File %s is of forbidden type %s" %
565
(os.path.join(tree_root, path), kind))
568
def bzr_inventory_data(tree):
569
inv_iter = tree.iter_inventory_ids(source=True, both=True)
571
for arch_id, path in inv_iter:
572
bzr_file_id = arch_id.replace('%', '%25').replace('/', '%2f')
573
inv_map[path] = bzr_file_id
576
for path, file_id in inv_map.iteritems():
577
full_path = os.path.join(tree, path)
578
kind = bzrlib.osutils.file_kind(full_path)
579
if kind not in ("file", "directory", "symlink"):
580
raise BadFileKind(tree, path, kind)
581
parent_dir = os.path.dirname(path)
583
parent_id = inv_map[parent_dir]
585
parent_id = bzrlib.inventory.ROOT_ID
586
bzr_inv.append((path, file_id, parent_id, kind))
591
class cmd_baz_import_branch(Command):
592
"""Import an Arch or Baz branch into a bzr branch"""
593
takes_args = ['to_location', 'from_branch?']
594
takes_options = ['verbose']
596
def printer(self, name):
599
def run(self, to_location, from_branch=None, fast=False, max_count=None,
600
verbose=False, dry_run=False):
601
to_location = os.path.realpath(str(to_location))
602
if from_branch is not None:
604
from_branch = pybaz.Version(from_branch)
605
except pybaz.errors.NamespaceError:
606
print "%s is not a valid Arch branch." % from_branch
608
import_version(to_location, from_branch, self.printer)
611
class cmd_baz_import(Command):
612
"""Import an Arch or Baz archive into bzr branches."""
613
takes_args = ['to_root_dir', 'from_archive']
614
takes_options = ['verbose']
616
def printer(self, name):
619
def run(self, to_root_dir, from_archive, verbose=False):
620
to_root = str(os.path.realpath(to_root_dir))
621
if not os.path.exists(to_root):
623
import_archive(to_root, from_archive, verbose, self.printer)
626
def import_archive(to_root, from_archive, verbose, printer):
627
for version in pybaz.Archive(str(from_archive)).iter_versions():
628
target = os.path.join(to_root, map_namespace(version))
629
printer("importing %s into %s" % (version, target))
630
if not os.path.exists(os.path.dirname(target)):
631
os.makedirs(os.path.dirname(target))
632
import_version(target, version, printer)
635
def map_namespace(a_version):
636
a_version = pybaz.Version("%s" % a_version)
637
parser = NameParser(a_version)
638
version = parser.get_version()
639
branch = parser.get_branch()
640
category = parser.get_category()
641
if branch is None or branch == '':
644
return "%s/%s" % (category, branch)
645
return "%s/%s/%s" % (category, version, branch)