1
# Copyright (C) 2004 Aaron Bentley
2
# <aaron.bentley@utoronto.ca>
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
28
from arch_core import chattermatch
32
__docformat__ = "restructuredtext"
33
__doc__ = "Compound functionality built on lower-level commands"
35
def iter_any_ancestry(tree, startrev):
36
return _iter_ancestry(tree, startrev)
38
def _iter_ancestry(tree, startrev):
39
"""Iterate over ancestry according to tree's patchlogs.
41
Generator that yields `arch.Patchlog` or `arch.Revision` instances
42
according to `tree` ancestry. Each item returned is the direct ancestor of
43
the previous `arch.Revision`. If `startrev` is supplied, it is the first
44
item yielded. The iteration stops after the first ancestor named in a
45
patchlog that does not, itself, have a patchlog in `tree`, or at the
48
:param tree: tree to determine the ancestry of
49
:type tree: `arch.ArchSourceTree`
50
:param startrev: revision to start at. Must be in the ancestry of `tree`.
51
:type startrev: `arch.Revision`
52
:rtype: iterator of `arch.Patchlog` or `arch.Revision`
55
while startrev is not None:
58
for log in tree.iter_logs(startrev.version, reverse=True):
59
if skip and log.revision == startrev:
64
if log.continuation_of:
65
nextrev = log.continuation_of # sure it should not be startrev?
66
# pretty sure. if we did that, startrev would never be None
69
if log and log.continuation_of:
70
yield log.continuation_of
73
def iter_ancestry(tree, startrev=None):
74
"""Iterate over ancestry according to tree's patchlogs.
76
Generator that yields `arch.Revision` instances according to `tree`
77
ancestry. Each `arch.Revision` returned is the direct ancestor of the
78
previous `arch.Revision`. If `startrev` is supplied, it is the first item
79
yielded. The iteration stops after the first ancestor named in a patchlog
80
that does not, itself, have a patchlog in `tree`, or at the beginning.
82
:param tree: tree to determine the ancestry of.
83
:type tree: `arch.ArchSourceTree`
84
:param startrev: optional revision to start at. Must be in the ancestry of
85
`tree`. The latest revision is used if not supplied.
86
:type startrev: `arch.Revision`
87
:rtype: iterator of `arch.Revision`
90
startrev = tree.tree_revision
91
for r in _iter_ancestry(tree, startrev):
92
if not isinstance(r, pybaz.Revision):
97
def iter_ancestry_logs(tree, startrev=None):
98
"""Iterate over ancestry according to tree's patchlogs.
100
Generator that yields `arch.Patchlog` instances according to `tree`
101
ancestry. Each `arch.Patchlog` returned is from the direct ancestor of the
102
revision of the previous `arch.Patchlog`. If `startrev` is supplied, it is
103
the first item yielded. The iteration stops after the first ancestor named
104
in a patchlog that does not, itself, have a patchlog in `tree`, or at the
107
:param tree: tree to determine the ancestry of.
108
:type tree: `arch.ArchSourceTree`
109
:param startrev: optional revision to start at. Must be in the ancestry of
110
`tree`. The latest revision is used if not supplied.
111
:type startrev: `arch.Revision`
112
:rtype: iterator of `arch.Patchlog`
115
startrev = tree.tree_revision
116
for r in _iter_ancestry(tree, startrev):
117
if not isinstance(r, pybaz.Patchlog):
118
r = pybaz.Patchlog(r)
122
def get_entry_name(entry):
123
"""Returns a filename for the entry, preferring the MOD name
125
:param entry: The entry to get a filename for
126
:type entry: `ChangesetEntry`
127
:return: The found file name
130
if entry.mod_name is not None:
131
return entry.mod_name
133
return entry.orig_name
135
def iter_changes(changeset_munger):
136
entries = changeset_munger.get_entries()
137
sorted_entries = list(entries.itervalues())
138
sorted_entries.sort(util.cmp_func(get_entry_name))
140
for entry in sorted_entries:
141
if entry.orig_name and not entry.mod_name:
142
if entry.orig_type == "dir":
146
yield pybaz.FileDeletion("D%s%s" % (suf, entry.orig_name[2:]), ' ')
148
for entry in sorted_entries:
149
if entry.mod_name and not entry.orig_name:
150
if entry.mod_type == "dir":
154
yield pybaz.FileAddition("A%s%s" % (suf, entry.mod_name[2:]), ' ')
156
for entry in sorted_entries:
157
if entry.orig_name != entry.mod_name and entry.orig_name and \
159
yield pybaz.FileRename("=> %s\t%s" % (entry.orig_name[2:],
160
entry.mod_name[2:]), ' ')
162
yield pybaz.FileModification("M %s" % entry.mod_name[2:], ' ')
164
if entry.original or entry.modified:
165
yield pybaz.FileModification("Mb %s" % entry.mod_name[2:], ' ')
167
if entry.orig_perms or entry.mod_perms:
168
if entry.mod_type == "file":
169
yield pybaz.FilePermissionsChange("-- %s" % entry.mod_name[2:],
171
elif entry.mod_type == "dir":
172
yield pybaz.FilePermissionsChange("-/ %s" % entry.mod_name[2:],
175
def diff_iter2(changeset,
176
exclude_str='(^\./{arch\}|/(\.arch-ids|\.arch-inventory))'):
177
"""Native diff iterator
179
:param changeset: The changeset to iterate through the diffs of
182
munger = native.ChangesetMunger(changeset)
183
munger.read_indices()
184
sorted_entries = list(munger.get_entries().itervalues())
185
sorted_entries.sort(util.cmp_func(get_entry_name))
186
if exclude_str is not None:
187
exclude = re.compile(exclude_str)
190
for entry in sorted_entries:
191
if exclude is not None and munger.entry_matches_pattern(entry, exclude):
193
if entry.diff is not None:
194
lines = [f.rstrip('\n') for f in open(entry.diff)]
195
elif entry.mod_name is None or entry.orig_name is None:
196
if entry.removed_file is None and entry.new_file is None:
199
diff = native.invoke_diff(changeset+"/removed-files-archive",
201
changeset+"/new-files-archive",
203
lines = diff.split('\n')
204
except native.BinaryFiles:
208
for line in native.diff_classifier(lines):
212
def replay(tree, revision, linehandler=None, reverse=False, munge_opts=None):
213
"""Applies the changeset for a revision to the tree
215
:param tree: The tree to apply changes for
216
:type tree: `arch.WorkingTree`
217
:param revision: The revision to get the changes for
218
:type revision: `arch.Revision`
219
:param reverse: Apply the changeset in reverse
221
:param munge_opts: options for munging the changeset before applying
222
:type munge_opts: `MungeOpts`
224
#revision = NativeRevision(str(revision))
225
my_dir = util.tmpdir(os.getcwd())
227
changeset = revision.get_patch(my_dir)
228
munger = native.ChangesetMunger(changeset)
229
munger.read_indices()
230
if munge_opts is not None:
231
munger.munge(munge_opts)
232
# entry_handler = PatchEntryHandler(tree)
234
iterator = iter_custom_apply_changeset(tree, munger, entry_handler, reverse=reverse)
235
for line in iterator:
236
if linehandler is not None:
239
if os.access(my_dir, os.R_OK):
240
shutil.rmtree(my_dir)
242
if iterator.conflicting:
250
def revert(tree, revision, munge_opts=None, keep=True):
251
"""Apply the difference between two things to tree.
253
:param tree: The tree to apply changes to
254
:type tree: `arch.ArchSourceTree`
255
:param revision: The revision to apply the changes to
256
:type revision: `arch.Revision`
257
:param munge_opts: If supplied, a set of parameters for munging the changeset
258
:type munge_opts: `MungeOpts`
259
:param keep: If true, keep the generated changeset
261
:rtype: Iterator of changeset application output
263
changeset = pybaz.util.new_numbered_name(tree, ',,undo-')
264
delt=pybaz.iter_delta(revision, tree, changeset)
266
if not isinstance(line, pybaz.TreeChange) and not chattermatch(line, "changeset:"):
268
if munge_opts is not None:
269
yield pybaz.Chatter("* munging changeset")
270
munger = native.ChangesetMunger(delt.changeset)
271
entries = munger.munge(munge_opts)
272
if entries == 0 and munge_opts.keep_pattern is not None:
273
print "No entries matched pattern %s" % munge_opts.keep_pattern
274
yield pybaz.Chatter("* applying changeset")
275
for line in delt.changeset.iter_apply(tree, reverse=True):
278
shutil.rmtree(changeset)
281
def iter_added_log(logs, revision):
282
"""Generate an iterator of revisions that added a certain revision log.
284
:param logs: An iterator of patchlogs to filter
285
:type logs: iter of `arch.Patchlog`
286
:param revision: The revision of the log to look for
287
:type revision: `arch.Revision`
288
:rtype: iter of `arch.Revision`
291
if revision in log.new_patches:
295
def merge_ancestor2(mine, other, other_revision):
296
"""Determines an ancestor suitable for merging, according to the tree logs.
298
:param mine: Tree to find an ancestor for (usually the working tree)
299
:type mine: `arch.WorkingTree`
300
:param other: Tree that merged or was merged by mine
301
:type other: `arch.ArchSourceTree`
302
:param other_revision: Override for broken library revisions
303
:type other_revision: `arch.Revision`
304
:return: Log of the merged revision
305
:rtype: `arch.Patchlog`
307
# find latest ancestor of opposite tree in each tree
308
my_revision=tree_latest(mine)
309
my_merged=last_merge_of(other, iter_ancestry(mine, my_revision))
310
other_merged=last_merge_of(mine, iter_ancestry(other, other_revision))
311
if my_merged is None:
312
if other_merged is None:
313
raise errors.UnrelatedTrees(mine, other)
315
if other_merged is None:
318
# determine which merge happend later in sequence for each tree
319
other_latest = latest_merge(other, other_revision, other_merged, my_merged)
320
my_latest = latest_merge(mine, my_revision, my_merged, other_merged)
321
if other_latest != my_latest:
322
#probably paralel merge
323
raise errors.NoLatestRevision(my_latest, other_latest)
328
def last_merge_of(into, ancestry):
329
"""Determine last log from the ancestry of "merged" in "into".
331
:param into: The tree that a log was merged into
332
:type into: `arch.ArchSourceTree`
333
:param ancestry: An interator of the tree's ancestry
334
:type ancestry: iter of `arch.Patchlog`
335
:return: The last merged log, or None if none found
336
:rtype: `arch.Revision`
341
for ancestor in ancestry:
342
if ancestor.version != log_version:
343
log_version = ancestor.version
344
log_list = list([f.revision for f in into.iter_logs(log_version)])
345
if ancestor in log_list:
350
def latest_merge(tree, tree_revision, tree_merged, other_merged):
351
"""Determines which of two revisions was merged most recently into tree
352
:param tree: The tree the revisions were merged into
353
:type tree: `arch.ArchSourceTree`
354
:param tree_merged: The revision from tree that was merged
355
:type tree_merged: `arch.Revision`
356
:param other_merged: The revision from OTHER that was merged
357
:type other_merged: `arch.Revision`
358
:return: The revision merged most recently, according to this tree
359
:rtype: `arch.Revision`
362
for log in iter_ancestry_logs(tree, tree_revision):
363
if log.revision == tree_merged:
365
# skip logs that are continuations, because of bogus new-patches
366
if log.continuation_of:
368
if other_merged in log.new_patches:
373
def iter_vchange(iterator):
374
"""First revision of each sequence from a different package-version.
376
Generator that yields only those revisions in different
377
package-versions than the previous revision.
379
:type iterator: iterator of `arch.Revision`
380
:rtype: iterator of `arch.Revision`
383
for item in iterator:
384
if oldversion is not None and item.version != oldversion:
386
oldversion = item.version
389
def tag_source(tree):
390
"""Return the revision that the current tree thinks it was tagged from.
392
:type tree: `arch.ArchSourceTree`
393
:rtype: `arch.Revision`
396
return iter_vchange(iter_ancestry(tree, None)).next()
397
except StopIteration:
401
def ensure_revision_exists(revision):
405
def ensure_archive_registered(archive, location=None):
406
"""Throw an exception if the archive isn't registered.
407
This function is intended to be overridden.
409
:param archive: The archive to check
410
:type archive: `arch.Archive`
411
:param location: A possible location of the archive
414
if archive.is_registered():
417
raise pybaz.errors.ArchiveNotRegistered(archive)
420
def tree_file_path(tree, path):
421
"""Convert a relative or absolute path into a tree-relative path
423
:param tree: The tree the path falls in
424
:param path: The path the tree falls in
426
dir, file = os.path.split(path)
429
dir = os.path.realpath(dir)
430
treedir = os.path.realpath(tree)
431
if not dir.startswith(treedir):
432
raise errors.TreeNotContain(tree, path)
433
dir = dir[len(treedir)+1:]
434
return os.path.join(dir, file)
437
"""Get the current directory expressed as a tree path.
438
:param tree: The tree that the current directory is in
439
:type tree: ArchSourceTree
440
:return: The tree path
441
:raise TreeNotContain: The path does not fall in the tree
444
if not cwd.startswith(str(tree)):
445
raise errors.TreeNotContain(tree, cwd)
446
treepath = cwd[len(str(tree)):]
447
treepath = treepath.lstrip('/')
451
def iter_changedfile(tree, revision, filename, yield_file=False):
452
"""Lists logs of ancestor revisions that changed a particular file.
453
Revisions that do not have patchlogs in the tree are ignored.
455
:param tree: The tree to get logs from
456
:type tree: `arch.ArchSourceTree`
457
:param revision: The revision to trace ancestry from
458
:type revision: `arch.Revision`
459
:param filename: The filename to look for changes to
461
:rtype: iterator of `arch.Patchlog`
465
filename = cwd + "/" + filename
466
for log in _iter_ancestry(tree, revision):
467
if isinstance(log, pybaz.Patchlog):
468
if filename in log.modified_files or filename in log.new_files or filename in log.removed_files:
470
yield (filename, log)
473
if log.renamed_files:
474
for (key, value) in log.renamed_files.iteritems():
475
if value == filename:
479
def iter_changed_file_line(tree, revision, filename, line_num):
480
"""Lists logs of ancestor revisions that changed a particular file.
481
Revisions that do not have patchlogs in the tree are ignored.
483
:param tree: The tree to get logs from
484
:type tree: `arch.ArchSourceTree`
485
:param revision: The revision to trace ancestry from
486
:type revision: `arch.Revision`
487
:param filename: The filename to look for changes to
489
:rtype: iterator of `arch.Patchlog`
496
for (filename, log) in iter_changedfile(tree, revision, filename, True):
498
if log.revision.patchlevel == "base-0" and log.continuation_of is None:
500
if filename in log.new_files:
502
if filename in log.removed_files:
504
patch = parse_changeset_patches(log.revision,[path])[0]
507
new_iter = patch.iter_inserted()
508
for (num, line) in new_iter:
511
for cur_patch in patches:
512
num = cur_patch.pos_in_mod(num)
515
if num == line_num - 1:
517
patches=[patch]+patches
521
"""A line associated with the log that produced it"""
522
def __init__(self, text, log=None):
527
def annotate_file(tree, revision, filename):
528
"""Lists logs of ancestor revisions that changed a particular file.
529
Revisions that do not have patchlogs in the tree are ignored.
531
:param tree: The tree to get logs from
532
:type tree: `arch.ArchSourceTree`
533
:param revision: The revision to trace ancestry from
534
:type revision: `arch.Revision`
535
:param filename: The filename to look for changes to
537
:rtype: iterator of `arch.Patchlog`
539
lines = [AnnotateLine(f) for f in open(filename)]
542
for (filename, log) in iter_changedfile(tree, revision, filename, True):
545
if log.revision.patchlevel == "base-0" and log.continuation_of is None:
547
if filename in log.new_files:
548
new_iter = iter_new_file(log.revision, path)
550
patch = parse_changeset_patches(log.revision,[path])[0]
553
new_iter = patch.iter_inserted()
554
for (num, line) in new_iter:
557
for cur_patch in patches:
558
num = cur_patch.pos_in_mod(num)
562
if num is not None and lines[num].log is None:
566
patches=[patch]+patches
570
def print_annotated(lines):
571
"""Unprettily-prints a set of annotated lines from annotate_file"""
572
last = "NotNoneOrPatchlog"
578
print line.log.revision
580
print(" " + line.text.rstrip("\n"))
582
def iter_changed_file_line_orig(tree, revision, filename, line_num):
583
"""Lists logs of revisions that originally added a line to a particular
584
file. Revisions that do not have patchlogs in the tree are ignored.
586
:param tree: The tree to get logs from
587
:type tree: `arch.ArchSourceTree`
588
:param revision: The revision to trace ancestry from
589
:type revision: `arch.Revision`
590
:param filename: The filename to look for changes to
592
:rtype: iterator of `arch.Patchlog`
594
path = tree_cwd(tree)+"/"+filename
595
my_file = open(filename)
597
for i in range(line_num):
598
line = my_file.next()
599
except StopIteration:
600
raise ValueError("line number out of range")
601
for log in iter_changed_file_line(tree, revision, filename, line_num):
602
yield merge_blame(log, filename, path, line)
605
def merge_blame(log, filename, path, line, diffs=None):
606
"""Return the log of the revision that added a line.
608
:param log: The log of the revision that added a line
609
:type log: `arch.Patchlog`
610
:param filename: The name of the file to look for
612
:param path: The path of the file to look for
614
:param line: The added line
617
for revision in log.new_patches:
618
if revision == log.revision:
620
if filename in log.modified_files or filename in log.new_files:
621
if patch_added_line(log.revision, filename, line, diffs):
622
return merge_blame(revision.patchlog, filename, path, line,
626
def get_diff(revision, path, diffs):
627
"""Returns the diff for a given path, memoizing it in diffs.
628
:param revision: The revision to find the diff for
629
:type revision: `arch.Revision`
630
:param path: The path of the diff to get
632
:param diffs: The diff will be placed in diffs if it's not already there
633
:type diffs: map of str => str
634
:return: lines of the diff
636
if diffs is not None:
637
diff = diffs.get(str(revision))
646
ensure_archive_registered(revision.archive)
647
changeset = revision.get_patch(tree+"/changeset")
648
patch_path = "%s/patches/%s.patch" % (changeset, path)
649
if not os.access(patch_path, os.R_OK):
651
diff = list(open(patch_path))
652
if diffs is not None:
653
diffs[str(revision)] = diff
658
def patch_added_line(revision, path, line, diffs=None):
659
"""Determine whether a revision's changeset added a line.
661
:param revision: The log of the revision that may have added a line
662
:type revision: `arch.Revision`
663
:param path: The path of the file to look for
665
:param line: The added line
668
diff = get_diff(revision, path, diffs)
672
match = (pline.startswith('+') and pline[1:] == line)
678
def parse_changeset_patches(revision, paths):
679
"""Retrieves the requested patches from a revision changeset.
681
:param revision: The revision of the changeset
682
:type revision: `arch.Revision`
683
:param paths: The path of the file to get patches for
685
:return: list of patches, in order of request
686
:rtype: list of `patches.Patch`
689
ensure_archive_registered(revision.archive)
690
changeset = revision.get_patch(tree+"/changeset")
693
patch_path = "%s/patches/%s.patch" % (changeset, path)
694
if os.path.exists(patch_path):
695
patchen.append(patches.parse_patch(open(patch_path)))
702
def iter_new_file(revision, path):
703
"""Retrieves the requested patches from a revision changeset.
705
:param revision: The revision of the changeset
706
:type revision: `arch.Revision`
707
:param paths: The path of the file to get patches for
709
:return: list of patches, in order of request
710
:rtype: list of `patches.Patch`
713
ensure_archive_registered(revision.archive)
714
changeset = revision.get_patch(tree+"/changeset")
715
file_path = "%s/new-files-archive/%s" % (changeset, path)
718
for line in open(file_path):
719
lines.append((i, line))
726
def iter_log_match(iter, key, value):
728
if isinstance(log, pybaz.Revision):
729
log = pybaz.Patchlog(log)
730
if log[key] == value:
734
def tree_latest(tree, version=None):
735
"""Return the latest Revision in the tree
737
:param tree: The tree to return the revision for
738
:type tree: `arch.ArchSourceTree`
739
:param version: The version of the revision to find
740
:type version: `arch.Version`
743
version = tree.tree_version
745
log = tree.iter_logs(version = version, reverse = True).next()
747
except StopIteration:
748
raise errors.NoVersionRevisions(tree, version)
750
def iter_all_library_revisions():
751
"""Generates an iterator of all revisions in the library
752
:return: iterator of all revisions in the revision library
753
:rtype: iter of `arch.Revision`
755
for archive in pybaz.iter_library_archives():
756
for category in archive.iter_library_categories():
757
for branch in category.iter_library_branches():
758
for version in branch.iter_library_versions():
759
for revision in version.iter_library_revisions():
763
def is_library_dir(directory):
764
directory = os.path.realpath(directory)
765
for revlib in arch_core.iter_revision_libraries():
771
def iter_greedy_libraries():
772
"""Generate an iterator of greedy libraries.
775
for path in arch_core.iter_revision_libraries():
776
if os.access("%s/=greedy" % path, os.R_OK):
780
def find_or_make_local_revision(revision):
781
"""Finds or makes a library copy of a revision.
782
:param revision: The revision to look for
783
:type revision: `arch.Revision`
784
:return: The library copy
785
:rtype: `arch.LibraryTree`
788
return revision.library_find()
790
ensure_revision_exists(revision)
791
for lib in iter_greedy_libraries():
792
list(arch_core.iter_library_add(revision, lib))
793
return revision.library_find()
794
raise errors.NoGreedy(revision)
796
def list_mod_names(tree):
797
tmpdir = util.tmpdir()+"/changeset"
800
for line in pybaz.iter_delta(tree_latest(tree), tree, tmpdir):
801
if isinstance(line, pybaz.TreeChange):
802
modified.append(line.name[1:])
804
shutil.rmtree(tmpdir)
807
def list_mod(tree, revision=None):
808
tmpdir = util.tmpdir()+"/changeset"
811
revision = tree_latest(tree)
813
for line in pybaz.iter_delta(revision, tree, tmpdir):
814
if isinstance(line, pybaz.TreeChange):
815
if line.name[0]==" ":
816
line.name = line.name[1:]
817
modified.append(line)
819
shutil.rmtree(tmpdir)
822
def valid_tree_root(tree):
824
raise errors.TreeRootNone()
827
def iter_no_conflicts(iter_rev, test_dir, good_dir, reverse=False):
828
"""Generate an iterator of revisions/changesets that don't conflict.
829
revision,changeset tuples are returned, but the changeset is None if
830
applying it would produce conflicts.
832
:param iter_rev: the iterator of revisions to go through
833
:type iter_rev: iterator of `arch.Revision`
834
:param test_dir: The scratch directory to use
836
:param good_dir: The good directory to copy from
838
:param reverse: Apply revisions in reverse?
840
:rtype: iter of (revision, changeset)
842
for revision in iter_rev:
845
if isinstance(revision, pybaz.Patchlog):
846
revision = revision.revision
847
directory = util.tmpdir()
848
tree = pybaz.tree_root(test_dir)
849
changeset = revision.get_patch(directory+"/changeset")
850
result = arch_core.apply_changeset(tree, changeset,
854
shutil.rmtree(test_dir)
855
util.linktree(good_dir, test_dir)
857
yield (revision, changeset)
858
shutil.rmtree(directory)
860
def iter_no_conflicts_rev(iter_rev, test_dir, good_dir, reverse=False):
861
"""Generate an iterator of revisions/changesets that don't conflict.
863
:param iter_rev: the iterator of revisions to go through
864
:type iter_rev: iterator of `arch.Revision`
865
:param test_dir: The scratch directory to use
867
:param good_dir: The good directory to copy from
869
:param reverse: Apply revisions in reverse?
871
:rtype: iter of revision
873
for (revision, changeset) in \
874
iter_no_conflicts(iter_rev, test_dir, good_dir, reverse):
875
if changeset is not None:
876
shutil.rmtree(changeset)
880
def iter_skip_replay_conflicts(tree, version):
881
"""Generate an iterator of revisions/changesets that won't conflict if
882
replayed. Assumes that all revisions returned are applied.
884
:param tree: the tree that revisions might conflict with
885
:type tree: `arch.WorkingTree`
887
:param reverse: Apply revisions in reverse?
889
iter_miss = arch_core.iter_missing(tree, version, False)
890
test_dir = util.tmpdir(tree)
892
util.linktree(tree, test_dir, top_exists = True)
893
iter_no_conf = iter_no_conflicts_rev(iter_miss, test_dir, tree)
894
return util.iter_delete_wrapper(iter_no_conf, test_dir)
896
shutil.rmtree(test_dir)
901
def __init__(self, anc_iter, nondeps=True):
902
self.anc_iter = anc_iter
905
revision = anc_iter.next()
906
if isinstance(revision, pybaz.Patchlog):
907
revision = revision.revision
908
self.treedir = util.tmpdir(os.getcwd())
909
self.good = self.treedir+"/good"
910
self.test = self.treedir+"/test"
911
list(arch_core.iter_get(revision, self.good))
912
util.linktree(self.good, self.test)
913
except StopIteration:
917
if self.treedir is not None:
918
shutil.rmtree(self.treedir)
921
if self.treedir is None:
924
for newrevision in self.anc_iter:
926
revision = newrevision
928
if isinstance(revision, pybaz.Patchlog):
929
revision = revision.revision
930
directory = util.tmpdir()
932
changeset = revision.get_patch(directory+"/changeset")
933
result = arch_core.apply_changeset(pybaz.tree_root(self.test),
934
changeset, reverse=True)
936
arch_core.apply_changeset(pybaz.tree_root(self.good),
937
changeset, reverse=True)
939
shutil.rmtree(directory)
942
shutil.rmtree(directory)
943
if (result == 0) == self.nondeps:
946
shutil.rmtree(self.test)
947
shutil.copytree(self.good, self.test)
948
revision = newrevision
952
"""Memoized access to tree logs"""
953
def __init__(self, tree):
957
def get_log(self, revision):
958
"""From tree, get the log for this revision.
960
:param revision: The revision to get
961
:type revision: `arch.Revision`
963
if not self.logs.has_key(revision.version):
964
self.logs[str(revision.version)] = \
965
list(self.tree.iter_logs(revision.version))
966
for log in self.logs[str(revision.version)]:
967
if log.revision == revision:
972
def iter_to_present(iter, tree):
973
"""Returns all logs until it encounters new-patch present in the tree
975
:param iter: The iterator that produces logs/revisions
976
:type iter: iter of `arch.Patchlog` or `arch.Revision`
978
log_memo = LogMemo(tree)
980
if isinstance(log, pybaz.Revision):
982
for revision in log.new_patches:
983
if log_memo.get_log(revision) is not None:
988
def changelog_for_merge(logs, merges):
989
"""Produces a new-format log-for-merge that includes merged log bodies
991
:param logs: The list or iter of logs to use. Direct merges recommended.
992
:type logs: iter of `arch.Patchlog`
993
:return: A string containing log-for-merge text
1000
changelog += " * %s\n" % log.revision
1001
changelog += " * %s\n" % log.summary
1002
if (log.description != ""):
1003
has_bogus_merges = False
1004
for merge in log.new_patches:
1005
if merges is not None and not merge in [x.revision for x in
1007
has_bogus_merges = True
1009
if not has_bogus_merges:
1010
changelog += log.description
1015
def __init__(self, tree):
1017
self.id_map = arch_core.get_id_filename_map(tree)
1019
class PatchEntryHandler(EntryHandler):
1020
def __init__(self, tree):
1021
EntryHandler.__init__(self, tree)
1022
self.removed_dir = None
1024
def __call__(self, entry, reverse):
1027
status = self.invoke_patch(entry, reverse)
1029
result = pybaz.FileModification("M %s" % entry.mod_name[2:], ' ')
1031
result = pybaz.PatchConflict("C %s" % entry.mod_name[2:])
1033
raise Exception("unexpected status %s" % str(status))
1034
os.unlink(entry.diff)
1037
def get_removed_dir(self):
1038
if self.removed_dir is None:
1039
self.removed_dir = tempfile.mkdtemp("", "+removed-conflict-files",
1041
return self.removed_dir
1043
def remove_conflict_files(self, file):
1045
if not os.path.exists(rej):
1048
if not os.path.exists(orig):
1050
if orig is not None or rej is not None:
1051
removed_dir = self.get_removed_dir()
1052
new_parent_dir = os.path.dirname(os.path.join(removed_dir,
1053
file[len(self.tree):]))
1054
if not os.path.exists(new_parent_dir):
1055
os.makedirs(new_parent_dir)
1057
if orig is not None:
1058
os.rename(orig, os.path.join(new_parent_dir,
1059
os.path.basename(orig)))
1061
os.rename(rej, os.path.join(new_parent_dir,
1062
os.path.basename(rej)))
1064
def invoke_patch(self, entry, reverse=False):
1065
file = os.path.join(self.tree, self.id_map[entry.id])
1066
self.remove_conflict_files(file)
1067
new_version = util.NewFileVersion(file)
1068
out_file = new_version.temp_filename
1069
status = native.invoke_patch(file, entry.diff, out_file, reverse)
1071
os.link(file, file+".orig")
1072
os.rename(out_file+".rej", file+".rej")
1073
new_version.commit()
1077
class Diff3Handler(PatchEntryHandler):
1078
def __init__(self, tree, older_tree, yours_tree):
1079
PatchEntryHandler.__init__(self, tree)
1080
self.older_tree = older_tree
1081
self.yours_tree = yours_tree
1082
self.older_map = arch_core.get_id_filename_map(self.older_tree)
1083
self.yours_map = arch_core.get_id_filename_map(self.yours_tree)
1085
def invoke_patch(self, entry, reverse=False):
1086
mine_file = os.path.join(self.tree, self.id_map[entry.id])
1087
older_path = os.path.join(self.older_tree, self.older_map[entry.id])
1088
yours_path = os.path.join(self.yours_tree, self.yours_map[entry.id])
1089
self.remove_conflict_files(mine_file)
1090
new_version = util.NewFileVersion(mine_file)
1091
out_file = new_version.temp_filename
1093
status = native.invoke_diff3(new_version, mine_file, yours_path,
1096
status = native.invoke_diff3(new_version, mine_file, older_path,
1099
new_version.commit()
1103
class iter_custom_apply_changeset:
1104
def __init__(self, dir, munger, entry_handler, reverse=False):
1106
self.munger = munger
1107
self.entry_handler = entry_handler
1108
self.reverse = reverse
1109
self.conflicting = False
1110
self.iterator = self.make_iterator()
1113
"""Destructively applies a changeset
1115
return self.iterator
1119
return self.iterator.next()
1122
def make_iterator(self):
1123
if self.entry_handler is not None:
1124
for entry in self.munger.get_entries().itervalues():
1125
result = self.entry_handler(entry, self.reverse)
1126
if result is not None:
1127
if isinstance(result, pybaz.PatchConflict):
1128
self.conflicting = True
1130
changeset = pybaz.Changeset(self.munger.changeset)
1131
ch_iter = changeset.iter_apply(self.dir, self.reverse)
1132
for line in ch_iter:
1134
if ch_iter.conflicting:
1135
self.conflicting = True
1137
def apply_delta_custom(a_spec, b_spec, tree, munge_opts=None, apply_type=None,
1139
def spec_resolve(spec):
1140
if isinstance(spec, pybaz.Revision):
1141
return find_or_make_local_revision(spec)
1145
from_path = spec_resolve(a_spec)
1146
to_path = spec_resolve(b_spec)
1148
if apply_type == PatchEntryHandler:
1149
entry_handler = PatchEntryHandler(tree, reverse=reverse)
1150
elif apply_type == Diff3Handler:
1151
entry_handler = Diff3Handler(tree, from_path, to_path)
1153
entry_handler = None
1155
tmp=util.tmpdir(tree)
1156
changeset=tmp+"/changeset"
1157
delt=pybaz.iter_delta(from_path, to_path, changeset)
1160
munger = native.ChangesetMunger(delt.changeset)
1161
munger.read_indices()
1162
if munge_opts is not None:
1163
yield pybaz.Chatter("* munging changeset")
1164
munger.munge(munge_opts)
1165
yield pybaz.Chatter("* applying changeset")
1166
for line in iter_custom_apply_changeset(tree, munger, entry_handler, reverse=reverse):
1173
return NativeRevision(name)
1174
except errors.UnsupportedScheme, e:
1175
return pybaz.Revision(name)
1178
class NativeRevision(pybaz.Revision):
1179
def __init__ (self, name):
1180
parsed = pybaz.NameParser(name)
1181
self.arch_name = parsed.get_archive()
1182
params = native.ArchParams()
1183
if not params['=locations'].exists(self.arch_name):
1184
raise pybaz.errors.ArchiveNotRegistered()
1185
self.location = params['=locations'][self.arch_name].strip()
1186
native.get_pfs_type(self.location)
1187
pybaz.Revision.__init__(self, name)
1190
return native.get_pfs_archive(self.location, self.arch_name)
1193
return self.narchive().exists(self)
1195
def get_patch(self, dir):
1196
tempdir = tempfile.mkdtemp("", ",",
1197
os.path.dirname(os.path.realpath(dir)))
1199
patchfile = os.path.join(tempdir + "delta.tar.gz")
1200
patch = native.cache_get_revision(self, "delta.tar.gz")
1202
patch = self.narchive().get_patch(self)
1203
file(patchfile, "wb").write(patch.read())
1204
parent_dir = util.untar_parent(patchfile, tempdir)
1205
os.rename(os.path.join(tempdir, parent_dir), dir)
1208
shutil.rmtree(tempdir)
1209
return pybaz.Changeset(dir)
1211
# arch-tag: c73c601a-0050-4e6c-ac63-819d7a00921f