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.commands import Command
20
from errors import NoPyBaz
24
from pybaz import NameParser as NameParser
25
from pybaz.backends.baz import null_cmd
28
from fai import iter_new_merges, direct_merges
34
from bzrlib.errors import BzrError
37
import bzrlib.inventory
41
from progress import *
43
def add_id(files, id=None):
44
"""Adds an explicit id to a list of files.
46
:param files: the name of the file to add an id to
47
:type files: list of str
48
:param id: tag one file using the specified id, instead of generating id
53
args.extend(["--id", id])
61
>>> q = test_environ()
64
>>> os.path.exists(os.path.join(q, "home", ".arch-params"))
66
>>> teardown_environ(q)
71
saved_dir = os.getcwdu()
72
tdir = tempfile.mkdtemp(prefix="testdir-")
73
os.environ["HOME"] = os.path.join(tdir, "home")
74
os.mkdir(os.environ["HOME"])
75
arch_dir = os.path.join(tdir, "archive_dir")
76
pybaz.make_archive("test@example.com", arch_dir)
77
work_dir = os.path.join(tdir, "work_dir")
80
pybaz.init_tree(work_dir, "test@example.com/test--test--0")
81
lib_dir = os.path.join(tdir, "lib_dir")
83
pybaz.register_revision_library(lib_dir)
84
pybaz.set_my_id("Test User<test@example.org>")
87
def add_file(path, text, id):
89
>>> q = test_environ()
90
>>> add_file("path with space", "text", "lalala")
91
>>> tree = pybaz.tree_root(".")
92
>>> inv = list(tree.iter_inventory_ids(source=True, both=True))
93
>>> ("x_lalala", "path with space") in inv
95
>>> teardown_environ(q)
97
file(path, "wb").write(text)
101
def add_dir(path, id):
103
>>> q = test_environ()
104
>>> add_dir("path with\(sp) space", "lalala")
105
>>> tree = pybaz.tree_root(".")
106
>>> inv = list(tree.iter_inventory_ids(source=True, both=True))
107
>>> ("x_lalala", "path with\(sp) space") in inv
109
>>> teardown_environ(q)
114
def teardown_environ(tdir):
118
def timport(tree, summary):
119
msg = tree.log_message()
120
msg["summary"] = summary
123
def commit(tree, summary):
125
>>> q = test_environ()
126
>>> tree = pybaz.tree_root(".")
127
>>> timport(tree, "import")
128
>>> commit(tree, "commit")
129
>>> logs = [str(l.revision) for l in tree.iter_logs()]
133
'test@example.com/test--test--0--base-0'
135
'test@example.com/test--test--0--patch-1'
136
>>> teardown_environ(q)
138
msg = tree.log_message()
139
msg["summary"] = summary
142
def commit_test_revisions():
144
>>> q = test_environ()
145
>>> commit_test_revisions()
146
>>> a = pybaz.Archive("test@example.com")
147
>>> revisions = list(a.iter_revisions("test--test--0"))
150
>>> str(revisions[2])
151
'test@example.com/test--test--0--base-0'
152
>>> str(revisions[1])
153
'test@example.com/test--test--0--patch-1'
154
>>> str(revisions[0])
155
'test@example.com/test--test--0--patch-2'
156
>>> teardown_environ(q)
158
tree = pybaz.tree_root(".")
159
add_file("mainfile", "void main(void){}", "mainfile by aaron")
160
timport(tree, "Created mainfile")
161
file("mainfile", "wb").write("or something like that")
162
commit(tree, "altered mainfile")
163
add_file("ofile", "this is another file", "ofile by aaron")
164
commit(tree, "altered mainfile")
167
def commit_more_test_revisions():
169
>>> q = test_environ()
170
>>> commit_test_revisions()
171
>>> commit_more_test_revisions()
172
>>> a = pybaz.Archive("test@example.com")
173
>>> revisions = list(a.iter_revisions("test--test--0"))
176
>>> str(revisions[0])
177
'test@example.com/test--test--0--patch-3'
178
>>> teardown_environ(q)
180
tree = pybaz.tree_root(".")
181
add_file("trainfile", "void train(void){}", "trainfile by aaron")
182
commit(tree, "altered trainfile")
184
class NoSuchVersion(Exception):
185
def __init__(self, version):
186
Exception.__init__(self, "The version %s does not exist." % version)
187
self.version = version
189
def version_ancestry(version):
191
>>> q = test_environ()
192
>>> commit_test_revisions()
193
>>> version = pybaz.Version("test@example.com/test--test--0")
194
>>> ancestors = version_ancestry(version)
195
>>> str(ancestors[0])
196
'test@example.com/test--test--0--base-0'
197
>>> str(ancestors[1])
198
'test@example.com/test--test--0--patch-1'
199
>>> version = pybaz.Version("test@example.com/test--test--0.5")
200
>>> ancestors = version_ancestry(version)
201
Traceback (most recent call last):
202
NoSuchVersion: The version test@example.com/test--test--0.5 does not exist.
203
>>> teardown_environ(q)
206
revision = version.iter_revisions(reverse=True).next()
209
if not version.exists():
210
raise NoSuchVersion(version)
213
ancestors = list(revision.iter_ancestors(metoo=True))
217
def get_last_revision(branch):
218
last_patch = branch.last_patch()
220
return arch_revision(last_patch)
221
except NotArchRevision:
223
"Directory \"%s\" already exists, and the last revision is not"
224
" an Arch revision (%s)" % (output_dir, last_patch))
227
def get_remaining_revisions(output_dir, version):
230
if os.path.exists(output_dir):
231
# We are starting from an existing directory, figure out what
232
# the current version is
233
branch = Branch.open(output_dir)
234
last_patch = get_last_revision(branch)
236
version = last_patch.version
237
elif version is None:
238
raise UserError("No version specified, and directory does not exist.")
241
ancestors = version_ancestry(version)
242
except NoSuchVersion, e:
246
for i in range(len(ancestors)):
247
if ancestors[i] == last_patch:
250
raise UserError("Directory \"%s\" already exists, and the last "
251
"revision (%s) is not in the ancestry of %s" %
252
(output_dir, last_patch, version))
253
# Strip off all of the ancestors which are already present
254
# And get a directory starting with the latest ancestor
255
latest_ancestor = ancestors[i]
256
old_revno = Branch.open(output_dir).revno()
257
ancestors = ancestors[i+1:]
258
return ancestors, old_revno
260
def import_version(output_dir, version, printer, fancy=True, fast=False,
261
verbose=False, dry_run=False, max_count=None):
263
>>> q = test_environ()
264
>>> result_path = os.path.join(q, "result")
265
>>> commit_test_revisions()
266
>>> version = pybaz.Version("test@example.com/test--test--0.1")
267
>>> def printer(message): print message
268
>>> import_version('/', version, printer, fancy=False, dry_run=True)
269
Traceback (most recent call last):
270
UserError: / exists, but is not a bzr branch.
271
>>> import_version(result_path, version, printer, fancy=False, dry_run=True)
272
Traceback (most recent call last):
273
UserError: The version test@example.com/test--test--0.1 does not exist.
274
>>> version = pybaz.Version("test@example.com/test--test--0")
275
>>> import_version(result_path, version, printer, fancy=False, dry_run=True)
278
Dry run, not modifying output_dir
280
>>> import_version(result_path, version, printer, fancy=False)
285
>>> import_version(result_path, version, printer, fancy=False)
286
Tree is up-to-date with test@example.com/test--test--0--patch-2
287
>>> commit_more_test_revisions()
288
>>> import_version(result_path, version, printer, fancy=False)
293
>>> teardown_environ(q)
296
ancestors, old_revno = get_remaining_revisions(output_dir, version)
297
except NotBranchError, e:
298
raise UserError("%s exists, but is not a bzr branch." % output_dir)
299
if len(ancestors) == 0:
300
last_revision = get_last_revision(Branch.open(output_dir))
301
print 'Tree is up-to-date with %s' % last_revision
304
progress_bar = ProgressBar()
305
tempdir = tempfile.mkdtemp(prefix="baz2bzr-",
306
dir=os.path.dirname(output_dir))
311
for result in iter_import_version(output_dir, ancestors, tempdir,
312
fast=fast, verbose=verbose, dry_run=dry_run,
313
max_count=max_count):
315
show_progress(progress_bar, result)
317
sys.stdout.write('.')
322
sys.stdout.write('\n')
325
print 'Dry run, not modifying output_dir'
327
if os.path.exists(output_dir):
328
# Move the bzr control directory back, and update the working tree
329
tmp_bzr_dir = os.path.join(tempdir, '.bzr')
331
bzr_dir = os.path.join(output_dir, '.bzr')
332
new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
334
os.rename(bzr_dir, tmp_bzr_dir) # Move the original bzr out of the way
335
os.rename(new_bzr_dir, bzr_dir)
337
bzrlib.merge.merge((output_dir, -1), (output_dir, None), # old_revno),
338
check_clean=False, this_dir=output_dir,
341
# If something failed, move back the original bzr directory
342
os.rename(bzr_dir, new_bzr_dir)
343
os.rename(tmp_bzr_dir, bzr_dir)
346
revdir = os.path.join(tempdir, "rd")
347
os.rename(revdir, output_dir)
350
printer('Cleaning up')
351
shutil.rmtree(tempdir)
352
printer("Import complete.")
354
class UserError(Exception):
355
def __init__(self, message):
356
"""Exception to throw when a user makes an impossible request
357
:param message: The message to emit when printing this exception
358
:type message: string
360
Exception.__init__(self, message)
362
def revision_id(arch_revision):
364
Generate a Bzr revision id from an Arch revision id. 'x' in the id
365
designates a revision imported with an experimental algorithm. A number
366
would indicate a particular standardized version.
368
:param arch_revision: The Arch revision to generate an ID for.
370
>>> revision_id(pybaz.Revision("you@example.com/cat--br--0--base-0"))
371
'Arch-1:you@example.com%cat--br--0--base-0'
373
return "Arch-1:%s" % str(arch_revision).replace('/', '%')
375
class NotArchRevision(Exception):
376
def __init__(self, revision_id):
377
msg = "The revision id %s does not look like it came from Arch."\
379
Exception.__init__(self, msg)
381
def arch_revision(revision_id):
383
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0"))
384
Traceback (most recent call last):
385
NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0 does not look like it came from Arch.
386
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--base-5"))
387
Traceback (most recent call last):
388
NotArchRevision: The revision id Arch-1:jrandom@example.com%test--test--0--base-5 does not look like it came from Arch.
389
>>> str(arch_revision("Arch-1:jrandom@example.com%test--test--0--patch-5"))
390
'jrandom@example.com/test--test--0--patch-5'
392
if revision_id is None:
394
if revision_id[:7] != 'Arch-1:':
395
raise NotArchRevision(revision_id)
398
return pybaz.Revision(revision_id[7:].replace('%', '/'))
399
except pybaz.errors.NamespaceError, e:
400
raise NotArchRevision(revision_id)
402
def iter_import_version(output_dir, ancestors, tempdir, fast=False,
403
verbose=False, dry_run=False, max_count=None):
406
# Uncomment this for testing, it basically just has baz2bzr only update
407
# 5 patches at a time
409
ancestors = ancestors[:max_count]
411
# Not sure if I want this output. basically it tells you ahead of time
412
# what it is going to do, but then later it tells you as it is doing it.
413
# what probably would be best would be to collapse it into ranges, so that
414
# this gives the simple view, and then later it gives the blow by blow.
416
# print 'Adding the following revisions:'
417
# for a in ancestors:
420
previous_version=None
421
missing_ancestor = None
423
for i in range(len(ancestors)):
424
revision = ancestors[i]
427
version = str(revision.version)
428
if version != previous_version:
430
print '\rOn version: %s' % version
431
yield Progress(str(revision.patchlevel), i, len(ancestors))
432
previous_version = version
434
yield Progress("revisions", i, len(ancestors))
436
revdir = os.path.join(tempdir, "rd")
438
baz_inv, log = get_revision(revdir, revision)
439
except pybaz.errors.ExecProblem, e:
440
if ("%s" % e.args).find('could not connect') == -1:
442
missing_ancestor = revision
444
print ("unable to access ancestor %s, making into a merge."
447
# cached so we can delete the log
449
log_summary = log.summary
450
log_creator = log.creator
451
if os.path.exists(output_dir):
452
bzr_dir = os.path.join(output_dir, '.bzr')
453
new_bzr_dir = os.path.join(tempdir, "rd", '.bzr')
454
# This would be much faster with a simple os.rename(), but if
455
# we fail, we have corrupted the original .bzr directory. Is
456
# that a big problem, as we can just back out the last
457
# revisions in .bzr/revision_history I don't really know
458
shutil.copytree(bzr_dir, new_bzr_dir)
459
# Now revdir should have a tree with the latest .bzr, and the
460
# next revision of the baz tree
461
branch = Branch.open(revdir)
463
branch = Branch.initialize(revdir)
465
old = os.path.join(revdir, ".bzr")
466
new = os.path.join(tempdir, ".bzr")
468
baz_inv, log = apply_revision(revdir, revision)
470
log_summary = log.summary
471
log_creator = log.creator
472
direct_merges = get_direct_merges(revdir, revision)
474
branch = Branch.open(revdir)
475
timestamp = email.Utils.mktime_tz(log_date + (0,))
476
rev_id = revision_id(revision)
477
if log_summary is None:
482
# if we want it to be in revision-history, do that here.
483
branch.add_pending_merge(revision_id(missing_ancestor))
484
missing_ancestor = None
485
for merge in direct_merges:
486
branch.add_pending_merge(revision_id(merge.revision))
487
branch.set_inventory(baz_inv)
488
bzrlib.trace.silent = True
489
branch.commit(log_summary, verbose=False, committer=log_creator,
490
timestamp=timestamp, timezone=0, rev_id=rev_id)
492
bzrlib.trace.silent = False
494
yield Progress("revisions", len(ancestors), len(ancestors))
495
unlink_unversioned(branch, revdir)
497
def get_direct_merges(revdir, revision):
498
if pybaz.WorkingTree(revdir).tree_version != revision.version:
499
pybaz.WorkingTree(revdir).set_tree_version(revision.version)
500
log_path = "%s/{arch}/%s/%s/%s/%s/patch-log/%s" % (revdir,
501
revision.category.nonarch, revision.branch.nonarch,
502
revision.version.nonarch, revision.archive, revision.patchlevel)
503
temp_path = tempfile.mktemp()
504
os.rename(log_path, temp_path)
505
merges = list(iter_new_merges(revdir, revision.version))
506
direct = direct_merges (merges)
507
os.rename(temp_path, log_path)
510
def unlink_unversioned(branch, revdir):
511
for unversioned in branch.working_tree().extras():
512
path = os.path.join(revdir, unversioned)
513
if os.path.isdir(path):
518
def get_log(tree, revision):
519
log = tree.iter_logs(version=revision.version, reverse=True).next()
520
assert str(log.revision) == str(revision), (log.revision, revision)
523
def get_revision(revdir, revision):
525
tree = pybaz.tree_root(revdir)
526
log = get_log(tree, revision)
528
return bzr_inventory_data(tree), log
529
except BadFileKind, e:
530
raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
533
def apply_revision(revdir, revision):
534
tree = pybaz.tree_root(revdir)
536
log = get_log(tree, revision)
538
return bzr_inventory_data(tree), log
539
except BadFileKind, e:
540
raise UserError("Cannot convert %s because %s is a %s" % (revision,e.path, e.kind) )
543
class BadFileKind(Exception):
544
"""The file kind is not permitted in bzr inventories"""
545
def __init__(self, tree_root, path, kind):
546
self.tree_root = tree_root
549
Exception.__init__(self, "File %s is of forbidden type %s" %
550
(os.path.join(tree_root, path), kind))
553
def bzr_inventory_data(tree):
554
inv_iter = tree.iter_inventory_ids(source=True, both=True)
556
for arch_id, path in inv_iter:
557
bzr_file_id = arch_id.replace('%', '%25').replace('/', '%2f')
558
inv_map[path] = bzr_file_id
561
for path, file_id in inv_map.iteritems():
562
full_path = os.path.join(tree, path)
563
kind = bzrlib.osutils.file_kind(full_path)
564
if kind not in ("file", "directory", "symlink"):
565
raise BadFileKind(tree, path, kind)
566
parent_dir = os.path.dirname(path)
568
parent_id = inv_map[parent_dir]
570
parent_id = bzrlib.inventory.ROOT_ID
571
bzr_inv.append((path, file_id, parent_id, kind))
576
class cmd_baz_import_branch(Command):
577
"""Import an Arch or Baz branch into a bzr branch"""
578
takes_args = ['to_location', 'from_branch?']
579
takes_options = ['verbose']
581
def printer(self, name):
584
def run(self, to_location, from_branch=None, fast=False, max_count=None,
585
verbose=False, dry_run=False):
586
to_location = os.path.realpath(str(to_location))
587
if from_branch is not None:
589
from_branch = pybaz.Version(from_branch)
590
except pybaz.errors.NamespaceError:
591
print "%s is not a valid Arch branch." % from_branch
593
import_version(to_location, from_branch, self.printer)
596
class cmd_baz_import(Command):
597
"""Import an Arch or Baz archive into bzr branches."""
598
takes_args = ['to_root_dir', 'from_archive']
599
takes_options = ['verbose']
601
def printer(self, name):
604
def run(self, to_root_dir, from_archive, verbose=False):
605
to_root = str(os.path.realpath(to_root_dir))
606
if not os.path.exists(to_root):
608
import_archive(to_root, from_archive, verbose, self.printer)
611
def import_archive(to_root, from_archive, verbose, printer):
612
for version in pybaz.Archive(str(from_archive)).iter_versions():
613
target = os.path.join(to_root, map_namespace(version))
614
printer("importing %s into %s" % (version, target))
615
if not os.path.exists(os.path.dirname(target)):
616
os.makedirs(os.path.dirname(target))
617
import_version(target, version, printer)
620
def map_namespace(a_version):
621
a_version = pybaz.Version("%s" % a_version)
622
parser = NameParser(a_version)
623
version = parser.get_version()
624
branch = parser.get_branch()
625
category = parser.get_category()
626
if branch is None or branch == '':
629
return "%s/%s" % (category, branch)
630
return "%s/%s/%s" % (category, version, branch)