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 ThreewayContentsConflict(Exception):
1087
def __init__(self, filename):
1088
msg = "Conflicting contents for file %s" % (filename)
1089
Exception.__init__(self, msg)
1092
class MissingForMerge(Exception):
1093
def __init__(self, filename):
1094
msg = "The file %s was modified, but does not exist in this tree"\
1096
Exception.__init__(self, msg)
1099
class ExceptionConflictHandler(object):
1100
"""Default handler for merge exceptions.
1102
This throws an error on any kind of conflict. Conflict handlers can
1103
descend from this class if they have a better way to handle some or
1104
all types of conflict.
1106
def missing_parent(self, pathname):
1107
parent = os.path.dirname(pathname)
1108
raise Exception("Parent directory missing for %s" % pathname)
1110
def dir_exists(self, pathname):
1111
raise Exception("Directory already exists for %s" % pathname)
1113
def failed_hunks(self, pathname):
1114
raise Exception("Failed to apply some hunks for %s" % pathname)
1116
def target_exists(self, entry, target, old_path):
1117
raise TargetExists(entry, target)
1119
def rename_conflict(self, id, this_name, base_name, other_name):
1120
raise RenameConflict(id, this_name, base_name, other_name)
1122
def move_conflict(self, id, this_dir, base_dir, other_dir):
1123
raise MoveConflict(id, this_dir, base_dir, other_dir)
1125
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1127
raise MergeConflict(this_path)
1129
def wrong_old_contents(self, filename, expected_contents):
1130
raise WrongOldContents(filename)
1132
def rem_contents_conflict(self, filename, this_contents, base_contents):
1133
raise RemoveContentsConflict(filename)
1135
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1136
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1138
def rmdir_non_empty(self, filename):
1139
raise DeletingNonEmptyDirectory(filename)
1141
def link_name_exists(self, filename):
1142
raise TargetExists(filename)
1144
def patch_target_missing(self, filename, contents):
1145
raise PatchTargetMissing(filename)
1147
def missing_for_exec_flag(self, filename):
1148
raise MissingForExecFlag(filename)
1150
def missing_for_rm(self, filename, change):
1151
raise MissingForRm(filename)
1153
def missing_for_rename(self, filename, to_path):
1154
raise MissingForRename(filename, to_path)
1156
def missing_for_merge(self, file_id, other_path):
1157
raise MissingForMerge(other_path)
1159
def new_contents_conflict(self, filename, other_contents):
1160
raise NewContentsConflict(filename)
1162
def threeway_contents_conflict(self, filename, this_contents,
1163
base_contents, other_contents):
1164
raise ThreewayContentsConflict(filename)
1169
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1171
"""Apply a changeset to a directory.
1173
:param changeset: The changes to perform
1174
:type changeset: `Changeset`
1175
:param inventory: The mapping of id to filename for the directory
1176
:type inventory: Dictionary
1177
:param dir: The path of the directory to apply the changes to
1179
:param reverse: If true, apply the changes in reverse
1181
:return: The mapping of the changed entries
1184
if conflict_handler is None:
1185
conflict_handler = ExceptionConflictHandler()
1186
temp_dir = os.path.join(dir, "bzr-tree-change")
1190
if e.errno == errno.EEXIST:
1194
if e.errno == errno.ENOTEMPTY:
1195
raise OldFailedTreeOp()
1200
#apply changes that don't affect filenames
1201
for entry in changeset.entries.itervalues():
1202
if not entry.is_creation_or_deletion() and not entry.is_boring():
1203
path = os.path.join(dir, inventory[entry.id])
1204
entry.apply(path, conflict_handler, reverse)
1206
# Apply renames in stages, to minimize conflicts:
1207
# Only files whose name or parent change are interesting, because their
1208
# target name may exist in the source tree. If a directory's name changes,
1209
# that doesn't make its children interesting.
1210
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1213
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1214
temp_dir, conflict_handler,
1217
rename_to_new_create(changed_inventory, target_entries, inventory,
1218
changeset, dir, conflict_handler, reverse)
1220
return changed_inventory
1223
def apply_changeset_tree(cset, tree, reverse=False):
1225
for entry in tree.source_inventory().itervalues():
1226
inventory[entry.id] = entry.path
1227
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1229
new_entries, remove_entries = \
1230
get_inventory_change(inventory, new_inventory, cset, reverse)
1231
tree.update_source_inventory(new_entries, remove_entries)
1234
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1237
for entry in cset.entries.itervalues():
1238
if entry.needs_rename():
1239
new_path = entry.get_new_path(inventory, cset)
1240
if new_path is None:
1241
remove_entries.append(entry.id)
1243
new_entries[new_path] = entry.id
1244
return new_entries, remove_entries
1247
def print_changeset(cset):
1248
"""Print all non-boring changeset entries
1250
:param cset: The changeset to print
1251
:type cset: `Changeset`
1253
for entry in cset.entries.itervalues():
1254
if entry.is_boring():
1257
print entry.summarize_name(cset)
1259
class CompositionFailure(Exception):
1260
def __init__(self, old_entry, new_entry, problem):
1261
msg = "Unable to conpose entries.\n %s" % problem
1262
Exception.__init__(self, msg)
1264
class IDMismatch(CompositionFailure):
1265
def __init__(self, old_entry, new_entry):
1266
problem = "Attempt to compose entries with different ids: %s and %s" %\
1267
(old_entry.id, new_entry.id)
1268
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1270
def compose_changesets(old_cset, new_cset):
1271
"""Combine two changesets into one. This works well for exact patching.
1272
Otherwise, not so well.
1274
:param old_cset: The first changeset that would be applied
1275
:type old_cset: `Changeset`
1276
:param new_cset: The second changeset that would be applied
1277
:type new_cset: `Changeset`
1278
:return: A changeset that combines the changes in both changesets
1281
composed = Changeset()
1282
for old_entry in old_cset.entries.itervalues():
1283
new_entry = new_cset.entries.get(old_entry.id)
1284
if new_entry is None:
1285
composed.add_entry(old_entry)
1287
composed_entry = compose_entries(old_entry, new_entry)
1288
if composed_entry.parent is not None or\
1289
composed_entry.new_parent is not None:
1290
composed.add_entry(composed_entry)
1291
for new_entry in new_cset.entries.itervalues():
1292
if not old_cset.entries.has_key(new_entry.id):
1293
composed.add_entry(new_entry)
1296
def compose_entries(old_entry, new_entry):
1297
"""Combine two entries into one.
1299
:param old_entry: The first entry that would be applied
1300
:type old_entry: ChangesetEntry
1301
:param old_entry: The second entry that would be applied
1302
:type old_entry: ChangesetEntry
1303
:return: A changeset entry combining both entries
1304
:rtype: `ChangesetEntry`
1306
if old_entry.id != new_entry.id:
1307
raise IDMismatch(old_entry, new_entry)
1308
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1310
if (old_entry.parent != old_entry.new_parent or
1311
new_entry.parent != new_entry.new_parent):
1312
output.new_parent = new_entry.new_parent
1314
if (old_entry.path != old_entry.new_path or
1315
new_entry.path != new_entry.new_path):
1316
output.new_path = new_entry.new_path
1318
output.contents_change = compose_contents(old_entry, new_entry)
1319
output.metadata_change = compose_metadata(old_entry, new_entry)
1322
def compose_contents(old_entry, new_entry):
1323
"""Combine the contents of two changeset entries. Entries are combined
1324
intelligently where possible, but the fallback behavior returns an
1327
:param old_entry: The first entry that would be applied
1328
:type old_entry: `ChangesetEntry`
1329
:param new_entry: The second entry that would be applied
1330
:type new_entry: `ChangesetEntry`
1331
:return: A combined contents change
1332
:rtype: anything supporting the apply(reverse=False) method
1334
old_contents = old_entry.contents_change
1335
new_contents = new_entry.contents_change
1336
if old_entry.contents_change is None:
1337
return new_entry.contents_change
1338
elif new_entry.contents_change is None:
1339
return old_entry.contents_change
1340
elif isinstance(old_contents, ReplaceContents) and \
1341
isinstance(new_contents, ReplaceContents):
1342
if old_contents.old_contents == new_contents.new_contents:
1345
return ReplaceContents(old_contents.old_contents,
1346
new_contents.new_contents)
1347
elif isinstance(old_contents, ApplySequence):
1348
output = ApplySequence(old_contents.changes)
1349
if isinstance(new_contents, ApplySequence):
1350
output.changes.extend(new_contents.changes)
1352
output.changes.append(new_contents)
1354
elif isinstance(new_contents, ApplySequence):
1355
output = ApplySequence((old_contents.changes,))
1356
output.extend(new_contents.changes)
1359
return ApplySequence((old_contents, new_contents))
1361
def compose_metadata(old_entry, new_entry):
1362
old_meta = old_entry.metadata_change
1363
new_meta = new_entry.metadata_change
1364
if old_meta is None:
1366
elif new_meta is None:
1368
elif (isinstance(old_meta, ChangeExecFlag) and
1369
isinstance(new_meta, ChangeExecFlag)):
1370
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1372
return ApplySequence(old_meta, new_meta)
1375
def changeset_is_null(changeset):
1376
for entry in changeset.entries.itervalues():
1377
if not entry.is_boring():
1381
class UnsupportedFiletype(Exception):
1382
def __init__(self, kind, full_path):
1383
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1385
Exception.__init__(self, msg)
1386
self.full_path = full_path
1389
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1390
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1393
class ChangesetGenerator(object):
1394
def __init__(self, tree_a, tree_b, interesting_ids=None):
1395
object.__init__(self)
1396
self.tree_a = tree_a
1397
self.tree_b = tree_b
1398
self._interesting_ids = interesting_ids
1400
def iter_both_tree_ids(self):
1401
for file_id in self.tree_a:
1403
for file_id in self.tree_b:
1404
if file_id not in self.tree_a:
1409
for file_id in self.iter_both_tree_ids():
1410
cs_entry = self.make_entry(file_id)
1411
if cs_entry is not None and not cs_entry.is_boring():
1412
cset.add_entry(cs_entry)
1414
for entry in list(cset.entries.itervalues()):
1415
if entry.parent != entry.new_parent:
1416
if not cset.entries.has_key(entry.parent) and\
1417
entry.parent != NULL_ID and entry.parent is not None:
1418
parent_entry = self.make_boring_entry(entry.parent)
1419
cset.add_entry(parent_entry)
1420
if not cset.entries.has_key(entry.new_parent) and\
1421
entry.new_parent != NULL_ID and \
1422
entry.new_parent is not None:
1423
parent_entry = self.make_boring_entry(entry.new_parent)
1424
cset.add_entry(parent_entry)
1427
def iter_inventory(self, tree):
1428
for file_id in tree:
1429
yield self.get_entry(file_id, tree)
1431
def get_entry(self, file_id, tree):
1432
if not tree.has_or_had_id(file_id):
1434
return tree.inventory[file_id]
1436
def get_entry_parent(self, entry):
1439
return entry.parent_id
1441
def get_path(self, file_id, tree):
1442
if not tree.has_or_had_id(file_id):
1444
path = tree.id2path(file_id)
1450
def make_basic_entry(self, file_id, only_interesting):
1451
entry_a = self.get_entry(file_id, self.tree_a)
1452
entry_b = self.get_entry(file_id, self.tree_b)
1453
if only_interesting and not self.is_interesting(entry_a, entry_b):
1455
parent = self.get_entry_parent(entry_a)
1456
path = self.get_path(file_id, self.tree_a)
1457
cs_entry = ChangesetEntry(file_id, parent, path)
1458
new_parent = self.get_entry_parent(entry_b)
1460
new_path = self.get_path(file_id, self.tree_b)
1462
cs_entry.new_path = new_path
1463
cs_entry.new_parent = new_parent
1466
def is_interesting(self, entry_a, entry_b):
1467
if self._interesting_ids is None:
1469
if entry_a is not None:
1470
file_id = entry_a.file_id
1471
elif entry_b is not None:
1472
file_id = entry_b.file_id
1475
return file_id in self._interesting_ids
1477
def make_boring_entry(self, id):
1478
cs_entry = self.make_basic_entry(id, only_interesting=False)
1479
if cs_entry.is_creation_or_deletion():
1480
return self.make_entry(id, only_interesting=False)
1485
def make_entry(self, id, only_interesting=True):
1486
cs_entry = self.make_basic_entry(id, only_interesting)
1488
if cs_entry is None:
1491
cs_entry.metadata_change = self.make_exec_flag_change(id)
1493
if id in self.tree_a and id in self.tree_b:
1494
a_sha1 = self.tree_a.get_file_sha1(id)
1495
b_sha1 = self.tree_b.get_file_sha1(id)
1496
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1499
cs_entry.contents_change = self.make_contents_change(id)
1502
def make_exec_flag_change(self, file_id):
1503
exec_flag_a = exec_flag_b = None
1504
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1505
exec_flag_a = self.tree_a.is_executable(file_id)
1507
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1508
exec_flag_b = self.tree_b.is_executable(file_id)
1510
if exec_flag_a == exec_flag_b:
1512
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1514
def make_contents_change(self, file_id):
1515
a_contents = get_contents(self.tree_a, file_id)
1516
b_contents = get_contents(self.tree_b, file_id)
1517
if a_contents == b_contents:
1519
return ReplaceContents(a_contents, b_contents)
1522
def get_contents(tree, file_id):
1523
"""Return the appropriate contents to create a copy of file_id from tree"""
1524
if file_id not in tree:
1526
kind = tree.kind(file_id)
1528
return TreeFileCreate(tree, file_id)
1529
elif kind in ("directory", "root_directory"):
1531
elif kind == "symlink":
1532
return SymlinkCreate(tree.get_symlink_target(file_id))
1534
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1537
def full_path(entry, tree):
1538
return os.path.join(tree.basedir, entry.path)
1540
def new_delete_entry(entry, tree, inventory, delete):
1541
if entry.path == "":
1544
parent = inventory[dirname(entry.path)].id
1545
cs_entry = ChangesetEntry(parent, entry.path)
1547
cs_entry.new_path = None
1548
cs_entry.new_parent = None
1550
cs_entry.path = None
1551
cs_entry.parent = None
1552
full_path = full_path(entry, tree)
1553
status = os.lstat(full_path)
1554
if stat.S_ISDIR(file_stat.st_mode):
1560
# XXX: Can't we unify this with the regular inventory object
1561
class Inventory(object):
1562
def __init__(self, inventory):
1563
self.inventory = inventory
1564
self.rinventory = None
1566
def get_rinventory(self):
1567
if self.rinventory is None:
1568
self.rinventory = invert_dict(self.inventory)
1569
return self.rinventory
1571
def get_path(self, id):
1572
return self.inventory.get(id)
1574
def get_name(self, id):
1575
path = self.get_path(id)
1579
return os.path.basename(path)
1581
def get_dir(self, id):
1582
path = self.get_path(id)
1587
return os.path.dirname(path)
1589
def get_parent(self, id):
1590
if self.get_path(id) is None:
1592
directory = self.get_dir(id)
1593
if directory == '.':
1595
if directory is None:
1597
return self.get_rinventory().get(directory)