1
# Copyright (C) 2004 Aaron Bentley <aaron.bentley@utoronto.ca>
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
20
from tempfile import mkdtemp
21
from shutil import rmtree
22
from bzrlib.trace import mutter
23
from bzrlib.osutils import rename, sha_file
25
from itertools import izip
27
# XXX: mbp: I'm not totally convinced that we should handle conflicts
28
# as part of changeset application, rather than only in the merge
31
"""Represent and apply a changeset
33
Conflicts in applying a changeset are represented as exceptions.
36
__docformat__ = "restructuredtext"
40
class OldFailedTreeOp(Exception):
42
Exception.__init__(self, "bzr-tree-change contains files from a"
43
" previous failed merge operation.")
44
def invert_dict(dict):
46
for (key,value) in dict.iteritems():
51
class ChangeExecFlag(object):
52
"""This is two-way change, suitable for file modification, creation,
54
def __init__(self, old_exec_flag, new_exec_flag):
55
self.old_exec_flag = old_exec_flag
56
self.new_exec_flag = new_exec_flag
58
def apply(self, filename, conflict_handler, reverse=False):
60
from_exec_flag = self.old_exec_flag
61
to_exec_flag = self.new_exec_flag
63
from_exec_flag = self.new_exec_flag
64
to_exec_flag = self.old_exec_flag
66
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
68
if e.errno == errno.ENOENT:
69
if conflict_handler.missing_for_exec_flag(filename) == "skip":
72
current_exec_flag = from_exec_flag
74
if from_exec_flag is not None and current_exec_flag != from_exec_flag:
75
if conflict_handler.wrong_old_exec_flag(filename,
76
from_exec_flag, current_exec_flag) != "continue":
79
if to_exec_flag is not None:
80
current_mode = os.stat(filename).st_mode
84
to_mode = current_mode | (0100 & ~umask)
85
# Enable x-bit for others only if they can read it.
86
if current_mode & 0004:
87
to_mode |= 0001 & ~umask
88
if current_mode & 0040:
89
to_mode |= 0010 & ~umask
91
to_mode = current_mode & ~0111
93
os.chmod(filename, to_mode)
95
if e.errno == errno.ENOENT:
96
conflict_handler.missing_for_exec_flag(filename)
98
def __eq__(self, other):
99
return (isinstance(other, ChangeExecFlag) and
100
self.old_exec_flag == other.old_exec_flag and
101
self.new_exec_flag == other.new_exec_flag)
103
def __ne__(self, other):
104
return not (self == other)
107
def dir_create(filename, conflict_handler, reverse):
108
"""Creates the directory, or deletes it if reverse is true. Intended to be
109
used with ReplaceContents.
111
:param filename: The name of the directory to create
113
:param reverse: If true, delete the directory, instead
120
if e.errno != errno.EEXIST:
122
if conflict_handler.dir_exists(filename) == "continue":
125
if e.errno == errno.ENOENT:
126
if conflict_handler.missing_parent(filename)=="continue":
127
file(filename, "wb").write(self.contents)
132
if e.errno != errno.ENOTEMPTY:
134
if conflict_handler.rmdir_non_empty(filename) == "skip":
139
class SymlinkCreate(object):
140
"""Creates or deletes a symlink (for use with ReplaceContents)"""
141
def __init__(self, contents):
144
:param contents: The filename of the target the symlink should point to
147
self.target = contents
150
return "SymlinkCreate(%s)" % self.target
152
def __call__(self, filename, conflict_handler, reverse):
153
"""Creates or destroys the symlink.
155
:param filename: The name of the symlink to create
159
assert(os.readlink(filename) == self.target)
163
os.symlink(self.target, filename)
165
if e.errno != errno.EEXIST:
167
if conflict_handler.link_name_exists(filename) == "continue":
168
os.symlink(self.target, filename)
170
def __eq__(self, other):
171
if not isinstance(other, SymlinkCreate):
173
elif self.target != other.target:
178
def __ne__(self, other):
179
return not (self == other)
181
class FileCreate(object):
182
"""Create or delete a file (for use with ReplaceContents)"""
183
def __init__(self, contents):
186
:param contents: The contents of the file to write
189
self.contents = contents
192
return "FileCreate(%i b)" % len(self.contents)
194
def __eq__(self, other):
195
if not isinstance(other, FileCreate):
197
elif self.contents != other.contents:
202
def __ne__(self, other):
203
return not (self == other)
205
def __call__(self, filename, conflict_handler, reverse):
206
"""Create or delete a file
208
:param filename: The name of the file to create
210
:param reverse: Delete the file instead of creating it
215
file(filename, "wb").write(self.contents)
217
if e.errno == errno.ENOENT:
218
if conflict_handler.missing_parent(filename)=="continue":
219
file(filename, "wb").write(self.contents)
225
if (file(filename, "rb").read() != self.contents):
226
direction = conflict_handler.wrong_old_contents(filename,
228
if direction != "continue":
232
if e.errno != errno.ENOENT:
234
if conflict_handler.missing_for_rm(filename, undo) == "skip":
239
class TreeFileCreate(object):
240
"""Create or delete a file (for use with ReplaceContents)"""
241
def __init__(self, tree, file_id):
244
:param contents: The contents of the file to write
248
self.file_id = file_id
251
return "TreeFileCreate(%s)" % self.file_id
253
def __eq__(self, other):
254
if not isinstance(other, TreeFileCreate):
256
return self.tree.get_file_sha1(self.file_id) == \
257
other.tree.get_file_sha1(other.file_id)
259
def __ne__(self, other):
260
return not (self == other)
262
def write_file(self, filename):
263
outfile = file(filename, "wb")
264
for line in self.tree.get_file(self.file_id):
267
def same_text(self, filename):
268
in_file = file(filename, "rb")
269
return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
271
def __call__(self, filename, conflict_handler, reverse):
272
"""Create or delete a file
274
:param filename: The name of the file to create
276
:param reverse: Delete the file instead of creating it
281
self.write_file(filename)
283
if e.errno == errno.ENOENT:
284
if conflict_handler.missing_parent(filename)=="continue":
285
self.write_file(filename)
291
if not self.same_text(filename):
292
direction = conflict_handler.wrong_old_contents(filename,
293
self.tree.get_file(self.file_id).read())
294
if direction != "continue":
298
if e.errno != errno.ENOENT:
300
if conflict_handler.missing_for_rm(filename, undo) == "skip":
305
def reversed(sequence):
306
max = len(sequence) - 1
307
for i in range(len(sequence)):
308
yield sequence[max - i]
310
class ReplaceContents(object):
311
"""A contents-replacement framework. It allows a file/directory/symlink to
312
be created, deleted, or replaced with another file/directory/symlink.
313
Arguments must be callable with (filename, reverse).
315
def __init__(self, old_contents, new_contents):
318
:param old_contents: The change to reverse apply (e.g. a deletion), \
320
:type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
322
:param new_contents: The second change to apply (e.g. a creation), \
324
:type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
327
self.old_contents=old_contents
328
self.new_contents=new_contents
331
return "ReplaceContents(%r -> %r)" % (self.old_contents,
334
def __eq__(self, other):
335
if not isinstance(other, ReplaceContents):
337
elif self.old_contents != other.old_contents:
339
elif self.new_contents != other.new_contents:
343
def __ne__(self, other):
344
return not (self == other)
346
def apply(self, filename, conflict_handler, reverse=False):
347
"""Applies the FileReplacement to the specified filename
349
:param filename: The name of the file to apply changes to
351
:param reverse: If true, apply the change in reverse
355
undo = self.old_contents
356
perform = self.new_contents
358
undo = self.new_contents
359
perform = self.old_contents
363
mode = os.lstat(filename).st_mode
364
if stat.S_ISLNK(mode):
367
if e.errno != errno.ENOENT:
369
if conflict_handler.missing_for_rm(filename, undo) == "skip":
371
undo(filename, conflict_handler, reverse=True)
372
if perform is not None:
373
perform(filename, conflict_handler, reverse=False)
375
os.chmod(filename, mode)
377
def is_creation(self):
378
return self.new_contents is not None and self.old_contents is None
380
def is_deletion(self):
381
return self.old_contents is not None and self.new_contents is None
383
class ApplySequence(object):
384
def __init__(self, changes=None):
386
if changes is not None:
387
self.changes.extend(changes)
389
def __eq__(self, other):
390
if not isinstance(other, ApplySequence):
392
elif len(other.changes) != len(self.changes):
395
for i in range(len(self.changes)):
396
if self.changes[i] != other.changes[i]:
400
def __ne__(self, other):
401
return not (self == other)
404
def apply(self, filename, conflict_handler, reverse=False):
408
iter = reversed(self.changes)
410
change.apply(filename, conflict_handler, reverse)
413
class Diff3Merge(object):
414
history_based = False
415
def __init__(self, file_id, base, other):
416
self.file_id = file_id
420
def is_creation(self):
423
def is_deletion(self):
426
def __eq__(self, other):
427
if not isinstance(other, Diff3Merge):
429
return (self.base == other.base and
430
self.other == other.other and self.file_id == other.file_id)
432
def __ne__(self, other):
433
return not (self == other)
435
def dump_file(self, temp_dir, name, tree):
436
out_path = os.path.join(temp_dir, name)
437
out_file = file(out_path, "wb")
438
in_file = tree.get_file(self.file_id)
443
def apply(self, filename, conflict_handler, reverse=False):
444
temp_dir = mkdtemp(prefix="bzr-")
446
new_file = filename+".new"
447
base_file = self.dump_file(temp_dir, "base", self.base)
448
other_file = self.dump_file(temp_dir, "other", self.other)
455
status = patch.diff3(new_file, filename, base, other)
457
os.chmod(new_file, os.stat(filename).st_mode)
458
rename(new_file, filename)
462
def get_lines(filename):
463
my_file = file(filename, "rb")
464
lines = my_file.readlines()
467
base_lines = get_lines(base)
468
other_lines = get_lines(other)
469
conflict_handler.merge_conflict(new_file, filename, base_lines,
476
"""Convenience function to create a directory.
478
:return: A ReplaceContents that will create a directory
479
:rtype: `ReplaceContents`
481
return ReplaceContents(None, dir_create)
484
"""Convenience function to delete a directory.
486
:return: A ReplaceContents that will delete a directory
487
:rtype: `ReplaceContents`
489
return ReplaceContents(dir_create, None)
491
def CreateFile(contents):
492
"""Convenience fucntion to create a file.
494
:param contents: The contents of the file to create
496
:return: A ReplaceContents that will create a file
497
:rtype: `ReplaceContents`
499
return ReplaceContents(None, FileCreate(contents))
501
def DeleteFile(contents):
502
"""Convenience fucntion to delete a file.
504
:param contents: The contents of the file to delete
506
:return: A ReplaceContents that will delete a file
507
:rtype: `ReplaceContents`
509
return ReplaceContents(FileCreate(contents), None)
511
def ReplaceFileContents(old_tree, new_tree, file_id):
512
"""Convenience fucntion to replace the contents of a file.
514
:param old_contents: The contents of the file to replace
515
:type old_contents: str
516
:param new_contents: The contents to replace the file with
517
:type new_contents: str
518
:return: A ReplaceContents that will replace the contents of a file a file
519
:rtype: `ReplaceContents`
521
return ReplaceContents(TreeFileCreate(old_tree, file_id),
522
TreeFileCreate(new_tree, file_id))
524
def CreateSymlink(target):
525
"""Convenience fucntion to create a symlink.
527
:param target: The path the link should point to
529
:return: A ReplaceContents that will delete a file
530
:rtype: `ReplaceContents`
532
return ReplaceContents(None, SymlinkCreate(target))
534
def DeleteSymlink(target):
535
"""Convenience fucntion to delete a symlink.
537
:param target: The path the link should point to
539
:return: A ReplaceContents that will delete a file
540
:rtype: `ReplaceContents`
542
return ReplaceContents(SymlinkCreate(target), None)
544
def ChangeTarget(old_target, new_target):
545
"""Convenience fucntion to change the target of a symlink.
547
:param old_target: The current link target
548
:type old_target: str
549
:param new_target: The new link target to use
550
:type new_target: str
551
:return: A ReplaceContents that will delete a file
552
:rtype: `ReplaceContents`
554
return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
557
class InvalidEntry(Exception):
558
"""Raise when a ChangesetEntry is invalid in some way"""
559
def __init__(self, entry, problem):
562
:param entry: The invalid ChangesetEntry
563
:type entry: `ChangesetEntry`
564
:param problem: The problem with the entry
567
msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id,
570
Exception.__init__(self, msg)
574
class SourceRootHasName(InvalidEntry):
575
"""This changeset entry has a name other than "", but its parent is !NULL"""
576
def __init__(self, entry, name):
579
:param entry: The invalid ChangesetEntry
580
:type entry: `ChangesetEntry`
581
:param name: The name of the entry
584
msg = 'Child of !NULL is named "%s", not "./.".' % name
585
InvalidEntry.__init__(self, entry, msg)
587
class NullIDAssigned(InvalidEntry):
588
"""The id !NULL was assigned to a real entry"""
589
def __init__(self, entry):
592
:param entry: The invalid ChangesetEntry
593
:type entry: `ChangesetEntry`
595
msg = '"!NULL" id assigned to a file "%s".' % entry.path
596
InvalidEntry.__init__(self, entry, msg)
598
class ParentIDIsSelf(InvalidEntry):
599
"""An entry is marked as its own parent"""
600
def __init__(self, entry):
603
:param entry: The invalid ChangesetEntry
604
:type entry: `ChangesetEntry`
606
msg = 'file %s has "%s" id for both self id and parent id.' % \
607
(entry.path, entry.id)
608
InvalidEntry.__init__(self, entry, msg)
610
class ChangesetEntry(object):
611
"""An entry the changeset"""
612
def __init__(self, id, parent, path):
613
"""Constructor. Sets parent and name assuming it was not
614
renamed/created/deleted.
615
:param id: The id associated with the entry
616
:param parent: The id of the parent of this entry (or !NULL if no
618
:param path: The file path relative to the tree root of this entry
624
self.new_parent = parent
625
self.contents_change = None
626
self.metadata_change = None
627
if parent == NULL_ID and path !='./.':
628
raise SourceRootHasName(self, path)
629
if self.id == NULL_ID:
630
raise NullIDAssigned(self)
631
if self.id == self.parent:
632
raise ParentIDIsSelf(self)
635
return "ChangesetEntry(%s)" % self.id
638
if self.path is None:
640
return os.path.dirname(self.path)
642
def __set_dir(self, dir):
643
self.path = os.path.join(dir, os.path.basename(self.path))
645
dir = property(__get_dir, __set_dir)
647
def __get_name(self):
648
if self.path is None:
650
return os.path.basename(self.path)
652
def __set_name(self, name):
653
self.path = os.path.join(os.path.dirname(self.path), name)
655
name = property(__get_name, __set_name)
657
def __get_new_dir(self):
658
if self.new_path is None:
660
return os.path.dirname(self.new_path)
662
def __set_new_dir(self, dir):
663
self.new_path = os.path.join(dir, os.path.basename(self.new_path))
665
new_dir = property(__get_new_dir, __set_new_dir)
667
def __get_new_name(self):
668
if self.new_path is None:
670
return os.path.basename(self.new_path)
672
def __set_new_name(self, name):
673
self.new_path = os.path.join(os.path.dirname(self.new_path), name)
675
new_name = property(__get_new_name, __set_new_name)
677
def needs_rename(self):
678
"""Determines whether the entry requires renaming.
683
return (self.parent != self.new_parent or self.name != self.new_name)
685
def is_deletion(self, reverse):
686
"""Return true if applying the entry would delete a file/directory.
688
:param reverse: if true, the changeset is being applied in reverse
691
return self.is_creation(not reverse)
693
def is_creation(self, reverse):
694
"""Return true if applying the entry would create a file/directory.
696
:param reverse: if true, the changeset is being applied in reverse
699
if self.contents_change is None:
702
return self.contents_change.is_deletion()
704
return self.contents_change.is_creation()
706
def is_creation_or_deletion(self):
707
"""Return true if applying the entry would create or delete a
712
return self.is_creation(False) or self.is_deletion(False)
714
def get_cset_path(self, mod=False):
715
"""Determine the path of the entry according to the changeset.
717
:param changeset: The changeset to derive the path from
718
:type changeset: `Changeset`
719
:param mod: If true, generate the MOD path. Otherwise, generate the \
721
:return: the path of the entry, or None if it did not exist in the \
723
:rtype: str or NoneType
726
if self.new_parent == NULL_ID:
728
elif self.new_parent is None:
732
if self.parent == NULL_ID:
734
elif self.parent is None:
738
def summarize_name(self, reverse=False):
739
"""Produce a one-line summary of the filename. Indicates renames as
740
old => new, indicates creation as None => new, indicates deletion as
743
:param changeset: The changeset to get paths from
744
:type changeset: `Changeset`
745
:param reverse: If true, reverse the names in the output
749
orig_path = self.get_cset_path(False)
750
mod_path = self.get_cset_path(True)
751
if orig_path is not None:
752
orig_path = orig_path[2:]
753
if mod_path is not None:
754
mod_path = mod_path[2:]
755
if orig_path == mod_path:
759
return "%s => %s" % (orig_path, mod_path)
761
return "%s => %s" % (mod_path, orig_path)
764
def get_new_path(self, id_map, changeset, reverse=False):
765
"""Determine the full pathname to rename to
767
:param id_map: The map of ids to filenames for the tree
768
:type id_map: Dictionary
769
:param changeset: The changeset to get data from
770
:type changeset: `Changeset`
771
:param reverse: If true, we're applying the changeset in reverse
775
mutter("Finding new path for %s" % self.summarize_name())
779
from_dir = self.new_dir
781
from_name = self.new_name
783
parent = self.new_parent
784
to_dir = self.new_dir
786
to_name = self.new_name
787
from_name = self.name
792
if parent == NULL_ID or parent is None:
794
raise SourceRootHasName(self, to_name)
797
if from_dir == to_dir:
798
dir = os.path.dirname(id_map[self.id])
800
mutter("path, new_path: %r %r" % (self.path, self.new_path))
801
parent_entry = changeset.entries[parent]
802
dir = parent_entry.get_new_path(id_map, changeset, reverse)
803
if from_name == to_name:
804
name = os.path.basename(id_map[self.id])
807
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
808
return os.path.join(dir, name)
811
"""Determines whether the entry does nothing
813
:return: True if the entry does no renames or content changes
816
if self.contents_change is not None:
818
elif self.metadata_change is not None:
820
elif self.parent != self.new_parent:
822
elif self.name != self.new_name:
827
def apply(self, filename, conflict_handler, reverse=False):
828
"""Applies the file content and/or metadata changes.
830
:param filename: the filename of the entry
832
:param reverse: If true, apply the changes in reverse
835
if self.is_deletion(reverse) and self.metadata_change is not None:
836
self.metadata_change.apply(filename, conflict_handler, reverse)
837
if self.contents_change is not None:
838
self.contents_change.apply(filename, conflict_handler, reverse)
839
if not self.is_deletion(reverse) and self.metadata_change is not None:
840
self.metadata_change.apply(filename, conflict_handler, reverse)
842
class IDPresent(Exception):
843
def __init__(self, id):
844
msg = "Cannot add entry because that id has already been used:\n%s" %\
846
Exception.__init__(self, msg)
849
class Changeset(object):
850
"""A set of changes to apply"""
854
def add_entry(self, entry):
855
"""Add an entry to the list of entries"""
856
if self.entries.has_key(entry.id):
857
raise IDPresent(entry.id)
858
self.entries[entry.id] = entry
860
def my_sort(sequence, key, reverse=False):
861
"""A sort function that supports supplying a key for comparison
863
:param sequence: The sequence to sort
864
:param key: A callable object that returns the values to be compared
865
:param reverse: If true, sort in reverse order
868
def cmp_by_key(entry_a, entry_b):
873
return cmp(key(entry_a), key(entry_b))
874
sequence.sort(cmp_by_key)
876
def get_rename_entries(changeset, inventory, reverse):
877
"""Return a list of entries that will be renamed. Entries are sorted from
878
longest to shortest source path and from shortest to longest target path.
880
:param changeset: The changeset to look in
881
:type changeset: `Changeset`
882
:param inventory: The source of current tree paths for the given ids
883
:type inventory: Dictionary
884
:param reverse: If true, the changeset is being applied in reverse
886
:return: source entries and target entries as a tuple
889
source_entries = [x for x in changeset.entries.itervalues()
890
if x.needs_rename() or x.is_creation_or_deletion()]
891
# these are done from longest path to shortest, to avoid deleting a
892
# parent before its children are deleted/renamed
893
def longest_to_shortest(entry):
894
path = inventory.get(entry.id)
899
my_sort(source_entries, longest_to_shortest, reverse=True)
901
target_entries = source_entries[:]
902
# These are done from shortest to longest path, to avoid creating a
903
# child before its parent has been created/renamed
904
def shortest_to_longest(entry):
905
path = entry.get_new_path(inventory, changeset, reverse)
910
my_sort(target_entries, shortest_to_longest)
911
return (source_entries, target_entries)
913
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
914
conflict_handler, reverse):
915
"""Delete and rename entries as appropriate. Entries are renamed to temp
916
names. A map of id -> temp name (or None, for deletions) is returned.
918
:param source_entries: The entries to rename and delete
919
:type source_entries: List of `ChangesetEntry`
920
:param inventory: The map of id -> filename in the current tree
921
:type inventory: Dictionary
922
:param dir: The directory to apply changes to
924
:param reverse: Apply changes in reverse
926
:return: a mapping of id to temporary name
930
for i in range(len(source_entries)):
931
entry = source_entries[i]
932
if entry.is_deletion(reverse):
933
path = os.path.join(dir, inventory[entry.id])
934
entry.apply(path, conflict_handler, reverse)
935
temp_name[entry.id] = None
937
elif entry.needs_rename():
938
to_name = os.path.join(temp_dir, str(i))
939
src_path = inventory.get(entry.id)
940
if src_path is not None:
941
src_path = os.path.join(dir, src_path)
943
rename(src_path, to_name)
944
temp_name[entry.id] = to_name
946
if e.errno != errno.ENOENT:
948
if conflict_handler.missing_for_rename(src_path, to_name) \
955
def rename_to_new_create(changed_inventory, target_entries, inventory,
956
changeset, dir, conflict_handler, reverse):
957
"""Rename entries with temp names to their final names, create new files.
959
:param changed_inventory: A mapping of id to temporary name
960
:type changed_inventory: Dictionary
961
:param target_entries: The entries to apply changes to
962
:type target_entries: List of `ChangesetEntry`
963
:param changeset: The changeset to apply
964
:type changeset: `Changeset`
965
:param dir: The directory to apply changes to
967
:param reverse: If true, apply changes in reverse
970
for entry in target_entries:
971
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
972
if new_tree_path is None:
974
new_path = os.path.join(dir, new_tree_path)
975
old_path = changed_inventory.get(entry.id)
976
if bzrlib.osutils.lexists(new_path):
977
if conflict_handler.target_exists(entry, new_path, old_path) == \
980
if entry.is_creation(reverse):
981
entry.apply(new_path, conflict_handler, reverse)
982
changed_inventory[entry.id] = new_tree_path
983
elif entry.needs_rename():
987
rename(old_path, new_path)
988
changed_inventory[entry.id] = new_tree_path
990
raise Exception ("%s is missing" % new_path)
992
class TargetExists(Exception):
993
def __init__(self, entry, target):
994
msg = "The path %s already exists" % target
995
Exception.__init__(self, msg)
999
class RenameConflict(Exception):
1000
def __init__(self, id, this_name, base_name, other_name):
1001
msg = """Trees all have different names for a file
1005
id: %s""" % (this_name, base_name, other_name, id)
1006
Exception.__init__(self, msg)
1007
self.this_name = this_name
1008
self.base_name = base_name
1009
self_other_name = other_name
1011
class MoveConflict(Exception):
1012
def __init__(self, id, this_parent, base_parent, other_parent):
1013
msg = """The file is in different directories in every tree
1017
id: %s""" % (this_parent, base_parent, other_parent, id)
1018
Exception.__init__(self, msg)
1019
self.this_parent = this_parent
1020
self.base_parent = base_parent
1021
self_other_parent = other_parent
1023
class MergeConflict(Exception):
1024
def __init__(self, this_path):
1025
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1026
self.this_path = this_path
1028
class WrongOldContents(Exception):
1029
def __init__(self, filename):
1030
msg = "Contents mismatch deleting %s" % filename
1031
self.filename = filename
1032
Exception.__init__(self, msg)
1034
class WrongOldExecFlag(Exception):
1035
def __init__(self, filename, old_exec_flag, new_exec_flag):
1036
msg = "Executable flag missmatch on %s:\n" \
1037
"Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
1038
self.filename = filename
1039
Exception.__init__(self, msg)
1041
class RemoveContentsConflict(Exception):
1042
def __init__(self, filename):
1043
msg = "Conflict deleting %s, which has different contents in BASE"\
1044
" and THIS" % filename
1045
self.filename = filename
1046
Exception.__init__(self, msg)
1048
class DeletingNonEmptyDirectory(Exception):
1049
def __init__(self, filename):
1050
msg = "Trying to remove dir %s while it still had files" % filename
1051
self.filename = filename
1052
Exception.__init__(self, msg)
1055
class PatchTargetMissing(Exception):
1056
def __init__(self, filename):
1057
msg = "Attempt to patch %s, which does not exist" % filename
1058
Exception.__init__(self, msg)
1059
self.filename = filename
1061
class MissingForSetExec(Exception):
1062
def __init__(self, filename):
1063
msg = "Attempt to change permissions on %s, which does not exist" %\
1065
Exception.__init__(self, msg)
1066
self.filename = filename
1068
class MissingForRm(Exception):
1069
def __init__(self, filename):
1070
msg = "Attempt to remove missing path %s" % filename
1071
Exception.__init__(self, msg)
1072
self.filename = filename
1075
class MissingForRename(Exception):
1076
def __init__(self, filename, to_path):
1077
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1078
Exception.__init__(self, msg)
1079
self.filename = filename
1081
class NewContentsConflict(Exception):
1082
def __init__(self, filename):
1083
msg = "Conflicting contents for new file %s" % (filename)
1084
Exception.__init__(self, msg)
1086
class WeaveMergeConflict(Exception):
1087
def __init__(self, filename):
1088
msg = "Conflicting contents for file %s" % (filename)
1089
Exception.__init__(self, msg)
1091
class ThreewayContentsConflict(Exception):
1092
def __init__(self, filename):
1093
msg = "Conflicting contents for file %s" % (filename)
1094
Exception.__init__(self, msg)
1097
class MissingForMerge(Exception):
1098
def __init__(self, filename):
1099
msg = "The file %s was modified, but does not exist in this tree"\
1101
Exception.__init__(self, msg)
1104
class ExceptionConflictHandler(object):
1105
"""Default handler for merge exceptions.
1107
This throws an error on any kind of conflict. Conflict handlers can
1108
descend from this class if they have a better way to handle some or
1109
all types of conflict.
1111
def missing_parent(self, pathname):
1112
parent = os.path.dirname(pathname)
1113
raise Exception("Parent directory missing for %s" % pathname)
1115
def dir_exists(self, pathname):
1116
raise Exception("Directory already exists for %s" % pathname)
1118
def failed_hunks(self, pathname):
1119
raise Exception("Failed to apply some hunks for %s" % pathname)
1121
def target_exists(self, entry, target, old_path):
1122
raise TargetExists(entry, target)
1124
def rename_conflict(self, id, this_name, base_name, other_name):
1125
raise RenameConflict(id, this_name, base_name, other_name)
1127
def move_conflict(self, id, this_dir, base_dir, other_dir):
1128
raise MoveConflict(id, this_dir, base_dir, other_dir)
1130
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1132
raise MergeConflict(this_path)
1134
def wrong_old_contents(self, filename, expected_contents):
1135
raise WrongOldContents(filename)
1137
def rem_contents_conflict(self, filename, this_contents, base_contents):
1138
raise RemoveContentsConflict(filename)
1140
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1141
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1143
def rmdir_non_empty(self, filename):
1144
raise DeletingNonEmptyDirectory(filename)
1146
def link_name_exists(self, filename):
1147
raise TargetExists(filename)
1149
def patch_target_missing(self, filename, contents):
1150
raise PatchTargetMissing(filename)
1152
def missing_for_exec_flag(self, filename):
1153
raise MissingForExecFlag(filename)
1155
def missing_for_rm(self, filename, change):
1156
raise MissingForRm(filename)
1158
def missing_for_rename(self, filename, to_path):
1159
raise MissingForRename(filename, to_path)
1161
def missing_for_merge(self, file_id, other_path):
1162
raise MissingForMerge(other_path)
1164
def new_contents_conflict(self, filename, other_contents):
1165
raise NewContentsConflict(filename)
1167
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1168
raise WeaveMergeConflict(filename)
1170
def threeway_contents_conflict(self, filename, this_contents,
1171
base_contents, other_contents):
1172
raise ThreewayContentsConflict(filename)
1177
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1179
"""Apply a changeset to a directory.
1181
:param changeset: The changes to perform
1182
:type changeset: `Changeset`
1183
:param inventory: The mapping of id to filename for the directory
1184
:type inventory: Dictionary
1185
:param dir: The path of the directory to apply the changes to
1187
:param reverse: If true, apply the changes in reverse
1189
:return: The mapping of the changed entries
1192
if conflict_handler is None:
1193
conflict_handler = ExceptionConflictHandler()
1194
temp_dir = os.path.join(dir, "bzr-tree-change")
1198
if e.errno == errno.EEXIST:
1202
if e.errno == errno.ENOTEMPTY:
1203
raise OldFailedTreeOp()
1208
#apply changes that don't affect filenames
1209
for entry in changeset.entries.itervalues():
1210
if not entry.is_creation_or_deletion() and not entry.is_boring():
1211
path = os.path.join(dir, inventory[entry.id])
1212
entry.apply(path, conflict_handler, reverse)
1214
# Apply renames in stages, to minimize conflicts:
1215
# Only files whose name or parent change are interesting, because their
1216
# target name may exist in the source tree. If a directory's name changes,
1217
# that doesn't make its children interesting.
1218
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1221
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1222
temp_dir, conflict_handler,
1225
rename_to_new_create(changed_inventory, target_entries, inventory,
1226
changeset, dir, conflict_handler, reverse)
1228
return changed_inventory
1231
def apply_changeset_tree(cset, tree, reverse=False):
1233
for entry in tree.source_inventory().itervalues():
1234
inventory[entry.id] = entry.path
1235
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1237
new_entries, remove_entries = \
1238
get_inventory_change(inventory, new_inventory, cset, reverse)
1239
tree.update_source_inventory(new_entries, remove_entries)
1242
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1245
for entry in cset.entries.itervalues():
1246
if entry.needs_rename():
1247
new_path = entry.get_new_path(inventory, cset)
1248
if new_path is None:
1249
remove_entries.append(entry.id)
1251
new_entries[new_path] = entry.id
1252
return new_entries, remove_entries
1255
def print_changeset(cset):
1256
"""Print all non-boring changeset entries
1258
:param cset: The changeset to print
1259
:type cset: `Changeset`
1261
for entry in cset.entries.itervalues():
1262
if entry.is_boring():
1265
print entry.summarize_name(cset)
1267
class CompositionFailure(Exception):
1268
def __init__(self, old_entry, new_entry, problem):
1269
msg = "Unable to conpose entries.\n %s" % problem
1270
Exception.__init__(self, msg)
1272
class IDMismatch(CompositionFailure):
1273
def __init__(self, old_entry, new_entry):
1274
problem = "Attempt to compose entries with different ids: %s and %s" %\
1275
(old_entry.id, new_entry.id)
1276
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1278
def compose_changesets(old_cset, new_cset):
1279
"""Combine two changesets into one. This works well for exact patching.
1280
Otherwise, not so well.
1282
:param old_cset: The first changeset that would be applied
1283
:type old_cset: `Changeset`
1284
:param new_cset: The second changeset that would be applied
1285
:type new_cset: `Changeset`
1286
:return: A changeset that combines the changes in both changesets
1289
composed = Changeset()
1290
for old_entry in old_cset.entries.itervalues():
1291
new_entry = new_cset.entries.get(old_entry.id)
1292
if new_entry is None:
1293
composed.add_entry(old_entry)
1295
composed_entry = compose_entries(old_entry, new_entry)
1296
if composed_entry.parent is not None or\
1297
composed_entry.new_parent is not None:
1298
composed.add_entry(composed_entry)
1299
for new_entry in new_cset.entries.itervalues():
1300
if not old_cset.entries.has_key(new_entry.id):
1301
composed.add_entry(new_entry)
1304
def compose_entries(old_entry, new_entry):
1305
"""Combine two entries into one.
1307
:param old_entry: The first entry that would be applied
1308
:type old_entry: ChangesetEntry
1309
:param old_entry: The second entry that would be applied
1310
:type old_entry: ChangesetEntry
1311
:return: A changeset entry combining both entries
1312
:rtype: `ChangesetEntry`
1314
if old_entry.id != new_entry.id:
1315
raise IDMismatch(old_entry, new_entry)
1316
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1318
if (old_entry.parent != old_entry.new_parent or
1319
new_entry.parent != new_entry.new_parent):
1320
output.new_parent = new_entry.new_parent
1322
if (old_entry.path != old_entry.new_path or
1323
new_entry.path != new_entry.new_path):
1324
output.new_path = new_entry.new_path
1326
output.contents_change = compose_contents(old_entry, new_entry)
1327
output.metadata_change = compose_metadata(old_entry, new_entry)
1330
def compose_contents(old_entry, new_entry):
1331
"""Combine the contents of two changeset entries. Entries are combined
1332
intelligently where possible, but the fallback behavior returns an
1335
:param old_entry: The first entry that would be applied
1336
:type old_entry: `ChangesetEntry`
1337
:param new_entry: The second entry that would be applied
1338
:type new_entry: `ChangesetEntry`
1339
:return: A combined contents change
1340
:rtype: anything supporting the apply(reverse=False) method
1342
old_contents = old_entry.contents_change
1343
new_contents = new_entry.contents_change
1344
if old_entry.contents_change is None:
1345
return new_entry.contents_change
1346
elif new_entry.contents_change is None:
1347
return old_entry.contents_change
1348
elif isinstance(old_contents, ReplaceContents) and \
1349
isinstance(new_contents, ReplaceContents):
1350
if old_contents.old_contents == new_contents.new_contents:
1353
return ReplaceContents(old_contents.old_contents,
1354
new_contents.new_contents)
1355
elif isinstance(old_contents, ApplySequence):
1356
output = ApplySequence(old_contents.changes)
1357
if isinstance(new_contents, ApplySequence):
1358
output.changes.extend(new_contents.changes)
1360
output.changes.append(new_contents)
1362
elif isinstance(new_contents, ApplySequence):
1363
output = ApplySequence((old_contents.changes,))
1364
output.extend(new_contents.changes)
1367
return ApplySequence((old_contents, new_contents))
1369
def compose_metadata(old_entry, new_entry):
1370
old_meta = old_entry.metadata_change
1371
new_meta = new_entry.metadata_change
1372
if old_meta is None:
1374
elif new_meta is None:
1376
elif (isinstance(old_meta, ChangeExecFlag) and
1377
isinstance(new_meta, ChangeExecFlag)):
1378
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1380
return ApplySequence(old_meta, new_meta)
1383
def changeset_is_null(changeset):
1384
for entry in changeset.entries.itervalues():
1385
if not entry.is_boring():
1389
class UnsupportedFiletype(Exception):
1390
def __init__(self, kind, full_path):
1391
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1393
Exception.__init__(self, msg)
1394
self.full_path = full_path
1397
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1398
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1401
class ChangesetGenerator(object):
1402
def __init__(self, tree_a, tree_b, interesting_ids=None):
1403
object.__init__(self)
1404
self.tree_a = tree_a
1405
self.tree_b = tree_b
1406
self._interesting_ids = interesting_ids
1408
def iter_both_tree_ids(self):
1409
for file_id in self.tree_a:
1411
for file_id in self.tree_b:
1412
if file_id not in self.tree_a:
1417
for file_id in self.iter_both_tree_ids():
1418
cs_entry = self.make_entry(file_id)
1419
if cs_entry is not None and not cs_entry.is_boring():
1420
cset.add_entry(cs_entry)
1422
for entry in list(cset.entries.itervalues()):
1423
if entry.parent != entry.new_parent:
1424
if not cset.entries.has_key(entry.parent) and\
1425
entry.parent != NULL_ID and entry.parent is not None:
1426
parent_entry = self.make_boring_entry(entry.parent)
1427
cset.add_entry(parent_entry)
1428
if not cset.entries.has_key(entry.new_parent) and\
1429
entry.new_parent != NULL_ID and \
1430
entry.new_parent is not None:
1431
parent_entry = self.make_boring_entry(entry.new_parent)
1432
cset.add_entry(parent_entry)
1435
def iter_inventory(self, tree):
1436
for file_id in tree:
1437
yield self.get_entry(file_id, tree)
1439
def get_entry(self, file_id, tree):
1440
if not tree.has_or_had_id(file_id):
1442
return tree.inventory[file_id]
1444
def get_entry_parent(self, entry):
1447
return entry.parent_id
1449
def get_path(self, file_id, tree):
1450
if not tree.has_or_had_id(file_id):
1452
path = tree.id2path(file_id)
1458
def make_basic_entry(self, file_id, only_interesting):
1459
entry_a = self.get_entry(file_id, self.tree_a)
1460
entry_b = self.get_entry(file_id, self.tree_b)
1461
if only_interesting and not self.is_interesting(entry_a, entry_b):
1463
parent = self.get_entry_parent(entry_a)
1464
path = self.get_path(file_id, self.tree_a)
1465
cs_entry = ChangesetEntry(file_id, parent, path)
1466
new_parent = self.get_entry_parent(entry_b)
1468
new_path = self.get_path(file_id, self.tree_b)
1470
cs_entry.new_path = new_path
1471
cs_entry.new_parent = new_parent
1474
def is_interesting(self, entry_a, entry_b):
1475
if self._interesting_ids is None:
1477
if entry_a is not None:
1478
file_id = entry_a.file_id
1479
elif entry_b is not None:
1480
file_id = entry_b.file_id
1483
return file_id in self._interesting_ids
1485
def make_boring_entry(self, id):
1486
cs_entry = self.make_basic_entry(id, only_interesting=False)
1487
if cs_entry.is_creation_or_deletion():
1488
return self.make_entry(id, only_interesting=False)
1493
def make_entry(self, id, only_interesting=True):
1494
cs_entry = self.make_basic_entry(id, only_interesting)
1496
if cs_entry is None:
1499
cs_entry.metadata_change = self.make_exec_flag_change(id)
1501
if id in self.tree_a and id in self.tree_b:
1502
a_sha1 = self.tree_a.get_file_sha1(id)
1503
b_sha1 = self.tree_b.get_file_sha1(id)
1504
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1507
cs_entry.contents_change = self.make_contents_change(id)
1510
def make_exec_flag_change(self, file_id):
1511
exec_flag_a = exec_flag_b = None
1512
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1513
exec_flag_a = self.tree_a.is_executable(file_id)
1515
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1516
exec_flag_b = self.tree_b.is_executable(file_id)
1518
if exec_flag_a == exec_flag_b:
1520
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1522
def make_contents_change(self, file_id):
1523
a_contents = get_contents(self.tree_a, file_id)
1524
b_contents = get_contents(self.tree_b, file_id)
1525
if a_contents == b_contents:
1527
return ReplaceContents(a_contents, b_contents)
1530
def get_contents(tree, file_id):
1531
"""Return the appropriate contents to create a copy of file_id from tree"""
1532
if file_id not in tree:
1534
kind = tree.kind(file_id)
1536
return TreeFileCreate(tree, file_id)
1537
elif kind in ("directory", "root_directory"):
1539
elif kind == "symlink":
1540
return SymlinkCreate(tree.get_symlink_target(file_id))
1542
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1545
def full_path(entry, tree):
1546
return os.path.join(tree.basedir, entry.path)
1548
def new_delete_entry(entry, tree, inventory, delete):
1549
if entry.path == "":
1552
parent = inventory[dirname(entry.path)].id
1553
cs_entry = ChangesetEntry(parent, entry.path)
1555
cs_entry.new_path = None
1556
cs_entry.new_parent = None
1558
cs_entry.path = None
1559
cs_entry.parent = None
1560
full_path = full_path(entry, tree)
1561
status = os.lstat(full_path)
1562
if stat.S_ISDIR(file_stat.st_mode):
1568
# XXX: Can't we unify this with the regular inventory object
1569
class Inventory(object):
1570
def __init__(self, inventory):
1571
self.inventory = inventory
1572
self.rinventory = None
1574
def get_rinventory(self):
1575
if self.rinventory is None:
1576
self.rinventory = invert_dict(self.inventory)
1577
return self.rinventory
1579
def get_path(self, id):
1580
return self.inventory.get(id)
1582
def get_name(self, id):
1583
path = self.get_path(id)
1587
return os.path.basename(path)
1589
def get_dir(self, id):
1590
path = self.get_path(id)
1595
return os.path.dirname(path)
1597
def get_parent(self, id):
1598
if self.get_path(id) is None:
1600
directory = self.get_dir(id)
1601
if directory == '.':
1603
if directory is None:
1605
return self.get_rinventory().get(directory)