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
17
"""Represent and apply a changeset.
19
Conflicts in applying a changeset are represented as exceptions.
21
This only handles the in-memory objects representing changesets, which are
22
primarily used by the merge code.
28
from tempfile import mkdtemp
29
from shutil import rmtree
30
from itertools import izip
32
from bzrlib.trace import mutter, warning
33
from bzrlib.osutils import rename, sha_file
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):
445
temp_dir = mkdtemp(prefix="bzr-")
447
new_file = filename+".new"
448
base_file = self.dump_file(temp_dir, "base", self.base)
449
other_file = self.dump_file(temp_dir, "other", self.other)
456
status = bzrlib.patch.diff3(new_file, filename, base, other)
458
os.chmod(new_file, os.stat(filename).st_mode)
459
rename(new_file, filename)
463
def get_lines(filename):
464
my_file = file(filename, "rb")
465
lines = my_file.readlines()
468
base_lines = get_lines(base)
469
other_lines = get_lines(other)
470
conflict_handler.merge_conflict(new_file, filename, base_lines,
477
"""Convenience function to create a directory.
479
:return: A ReplaceContents that will create a directory
480
:rtype: `ReplaceContents`
482
return ReplaceContents(None, dir_create)
485
"""Convenience function to delete a directory.
487
:return: A ReplaceContents that will delete a directory
488
:rtype: `ReplaceContents`
490
return ReplaceContents(dir_create, None)
492
def CreateFile(contents):
493
"""Convenience fucntion to create a file.
495
:param contents: The contents of the file to create
497
:return: A ReplaceContents that will create a file
498
:rtype: `ReplaceContents`
500
return ReplaceContents(None, FileCreate(contents))
502
def DeleteFile(contents):
503
"""Convenience fucntion to delete a file.
505
:param contents: The contents of the file to delete
507
:return: A ReplaceContents that will delete a file
508
:rtype: `ReplaceContents`
510
return ReplaceContents(FileCreate(contents), None)
512
def ReplaceFileContents(old_tree, new_tree, file_id):
513
"""Convenience fucntion to replace the contents of a file.
515
:param old_contents: The contents of the file to replace
516
:type old_contents: str
517
:param new_contents: The contents to replace the file with
518
:type new_contents: str
519
:return: A ReplaceContents that will replace the contents of a file a file
520
:rtype: `ReplaceContents`
522
return ReplaceContents(TreeFileCreate(old_tree, file_id),
523
TreeFileCreate(new_tree, file_id))
525
def CreateSymlink(target):
526
"""Convenience fucntion to create a symlink.
528
:param target: The path the link should point to
530
:return: A ReplaceContents that will delete a file
531
:rtype: `ReplaceContents`
533
return ReplaceContents(None, SymlinkCreate(target))
535
def DeleteSymlink(target):
536
"""Convenience fucntion to delete a symlink.
538
:param target: The path the link should point to
540
:return: A ReplaceContents that will delete a file
541
:rtype: `ReplaceContents`
543
return ReplaceContents(SymlinkCreate(target), None)
545
def ChangeTarget(old_target, new_target):
546
"""Convenience fucntion to change the target of a symlink.
548
:param old_target: The current link target
549
:type old_target: str
550
:param new_target: The new link target to use
551
:type new_target: str
552
:return: A ReplaceContents that will delete a file
553
:rtype: `ReplaceContents`
555
return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
558
class InvalidEntry(Exception):
559
"""Raise when a ChangesetEntry is invalid in some way"""
560
def __init__(self, entry, problem):
563
:param entry: The invalid ChangesetEntry
564
:type entry: `ChangesetEntry`
565
:param problem: The problem with the entry
568
msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id,
571
Exception.__init__(self, msg)
575
class SourceRootHasName(InvalidEntry):
576
"""This changeset entry has a name other than "", but its parent is !NULL"""
577
def __init__(self, entry, name):
580
:param entry: The invalid ChangesetEntry
581
:type entry: `ChangesetEntry`
582
:param name: The name of the entry
585
msg = 'Child of !NULL is named "%s", not "./.".' % name
586
InvalidEntry.__init__(self, entry, msg)
588
class NullIDAssigned(InvalidEntry):
589
"""The id !NULL was assigned to a real entry"""
590
def __init__(self, entry):
593
:param entry: The invalid ChangesetEntry
594
:type entry: `ChangesetEntry`
596
msg = '"!NULL" id assigned to a file "%s".' % entry.path
597
InvalidEntry.__init__(self, entry, msg)
599
class ParentIDIsSelf(InvalidEntry):
600
"""An entry is marked as its own parent"""
601
def __init__(self, entry):
604
:param entry: The invalid ChangesetEntry
605
:type entry: `ChangesetEntry`
607
msg = 'file %s has "%s" id for both self id and parent id.' % \
608
(entry.path, entry.id)
609
InvalidEntry.__init__(self, entry, msg)
611
class ChangesetEntry(object):
612
"""An entry the changeset"""
613
def __init__(self, id, parent, path):
614
"""Constructor. Sets parent and name assuming it was not
615
renamed/created/deleted.
616
:param id: The id associated with the entry
617
:param parent: The id of the parent of this entry (or !NULL if no
619
:param path: The file path relative to the tree root of this entry
625
self.new_parent = parent
626
self.contents_change = None
627
self.metadata_change = None
628
if parent == NULL_ID and path !='./.':
629
raise SourceRootHasName(self, path)
630
if self.id == NULL_ID:
631
raise NullIDAssigned(self)
632
if self.id == self.parent:
633
raise ParentIDIsSelf(self)
636
return "ChangesetEntry(%s)" % self.id
639
if self.path is None:
641
return os.path.dirname(self.path)
643
def __set_dir(self, dir):
644
self.path = os.path.join(dir, os.path.basename(self.path))
646
dir = property(__get_dir, __set_dir)
648
def __get_name(self):
649
if self.path is None:
651
return os.path.basename(self.path)
653
def __set_name(self, name):
654
self.path = os.path.join(os.path.dirname(self.path), name)
656
name = property(__get_name, __set_name)
658
def __get_new_dir(self):
659
if self.new_path is None:
661
return os.path.dirname(self.new_path)
663
def __set_new_dir(self, dir):
664
self.new_path = os.path.join(dir, os.path.basename(self.new_path))
666
new_dir = property(__get_new_dir, __set_new_dir)
668
def __get_new_name(self):
669
if self.new_path is None:
671
return os.path.basename(self.new_path)
673
def __set_new_name(self, name):
674
self.new_path = os.path.join(os.path.dirname(self.new_path), name)
676
new_name = property(__get_new_name, __set_new_name)
678
def needs_rename(self):
679
"""Determines whether the entry requires renaming.
684
return (self.parent != self.new_parent or self.name != self.new_name)
686
def is_deletion(self, reverse):
687
"""Return true if applying the entry would delete a file/directory.
689
:param reverse: if true, the changeset is being applied in reverse
692
return self.is_creation(not reverse)
694
def is_creation(self, reverse):
695
"""Return true if applying the entry would create a file/directory.
697
:param reverse: if true, the changeset is being applied in reverse
700
if self.contents_change is None:
703
return self.contents_change.is_deletion()
705
return self.contents_change.is_creation()
707
def is_creation_or_deletion(self):
708
"""Return true if applying the entry would create or delete a
713
return self.is_creation(False) or self.is_deletion(False)
715
def get_cset_path(self, mod=False):
716
"""Determine the path of the entry according to the changeset.
718
:param changeset: The changeset to derive the path from
719
:type changeset: `Changeset`
720
:param mod: If true, generate the MOD path. Otherwise, generate the \
722
:return: the path of the entry, or None if it did not exist in the \
724
:rtype: str or NoneType
727
if self.new_parent == NULL_ID:
729
elif self.new_parent is None:
733
if self.parent == NULL_ID:
735
elif self.parent is None:
739
def summarize_name(self, reverse=False):
740
"""Produce a one-line summary of the filename. Indicates renames as
741
old => new, indicates creation as None => new, indicates deletion as
744
:param changeset: The changeset to get paths from
745
:type changeset: `Changeset`
746
:param reverse: If true, reverse the names in the output
750
orig_path = self.get_cset_path(False)
751
mod_path = self.get_cset_path(True)
752
if orig_path is not None:
753
orig_path = orig_path[2:]
754
if mod_path is not None:
755
mod_path = mod_path[2:]
756
if orig_path == mod_path:
760
return "%s => %s" % (orig_path, mod_path)
762
return "%s => %s" % (mod_path, orig_path)
765
def get_new_path(self, id_map, changeset, reverse=False):
766
"""Determine the full pathname to rename to
768
:param id_map: The map of ids to filenames for the tree
769
:type id_map: Dictionary
770
:param changeset: The changeset to get data from
771
:type changeset: `Changeset`
772
:param reverse: If true, we're applying the changeset in reverse
776
mutter("Finding new path for %s", self.summarize_name())
780
from_dir = self.new_dir
782
from_name = self.new_name
784
parent = self.new_parent
785
to_dir = self.new_dir
787
to_name = self.new_name
788
from_name = self.name
793
if parent == NULL_ID or parent is None:
795
raise SourceRootHasName(self, to_name)
798
if from_dir == to_dir:
799
dir = os.path.dirname(id_map[self.id])
801
mutter("path, new_path: %r %r", self.path, self.new_path)
802
parent_entry = changeset.entries[parent]
803
dir = parent_entry.get_new_path(id_map, changeset, reverse)
804
if from_name == to_name:
805
name = os.path.basename(id_map[self.id])
808
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
809
return os.path.join(dir, name)
812
"""Determines whether the entry does nothing
814
:return: True if the entry does no renames or content changes
817
if self.contents_change is not None:
819
elif self.metadata_change is not None:
821
elif self.parent != self.new_parent:
823
elif self.name != self.new_name:
828
def apply(self, filename, conflict_handler, reverse=False):
829
"""Applies the file content and/or metadata changes.
831
:param filename: the filename of the entry
833
:param reverse: If true, apply the changes in reverse
836
if self.is_deletion(reverse) and self.metadata_change is not None:
837
self.metadata_change.apply(filename, conflict_handler, reverse)
838
if self.contents_change is not None:
839
self.contents_change.apply(filename, conflict_handler, reverse)
840
if not self.is_deletion(reverse) and self.metadata_change is not None:
841
self.metadata_change.apply(filename, conflict_handler, reverse)
843
class IDPresent(Exception):
844
def __init__(self, id):
845
msg = "Cannot add entry because that id has already been used:\n%s" %\
847
Exception.__init__(self, msg)
850
class Changeset(object):
851
"""A set of changes to apply"""
855
def add_entry(self, entry):
856
"""Add an entry to the list of entries"""
857
if self.entries.has_key(entry.id):
858
raise IDPresent(entry.id)
859
self.entries[entry.id] = entry
861
def my_sort(sequence, key, reverse=False):
862
"""A sort function that supports supplying a key for comparison
864
:param sequence: The sequence to sort
865
:param key: A callable object that returns the values to be compared
866
:param reverse: If true, sort in reverse order
869
def cmp_by_key(entry_a, entry_b):
874
return cmp(key(entry_a), key(entry_b))
875
sequence.sort(cmp_by_key)
877
def get_rename_entries(changeset, inventory, reverse):
878
"""Return a list of entries that will be renamed. Entries are sorted from
879
longest to shortest source path and from shortest to longest target path.
881
:param changeset: The changeset to look in
882
:type changeset: `Changeset`
883
:param inventory: The source of current tree paths for the given ids
884
:type inventory: Dictionary
885
:param reverse: If true, the changeset is being applied in reverse
887
:return: source entries and target entries as a tuple
890
source_entries = [x for x in changeset.entries.itervalues()
891
if x.needs_rename() or x.is_creation_or_deletion()]
892
# these are done from longest path to shortest, to avoid deleting a
893
# parent before its children are deleted/renamed
894
def longest_to_shortest(entry):
895
path = inventory.get(entry.id)
900
my_sort(source_entries, longest_to_shortest, reverse=True)
902
target_entries = source_entries[:]
903
# These are done from shortest to longest path, to avoid creating a
904
# child before its parent has been created/renamed
905
def shortest_to_longest(entry):
906
path = entry.get_new_path(inventory, changeset, reverse)
911
my_sort(target_entries, shortest_to_longest)
912
return (source_entries, target_entries)
914
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
915
conflict_handler, reverse):
916
"""Delete and rename entries as appropriate. Entries are renamed to temp
917
names. A map of id -> temp name (or None, for deletions) is returned.
919
:param source_entries: The entries to rename and delete
920
:type source_entries: List of `ChangesetEntry`
921
:param inventory: The map of id -> filename in the current tree
922
:type inventory: Dictionary
923
:param dir: The directory to apply changes to
925
:param reverse: Apply changes in reverse
927
:return: a mapping of id to temporary name
931
for i in range(len(source_entries)):
932
entry = source_entries[i]
933
if entry.is_deletion(reverse):
934
path = os.path.join(dir, inventory[entry.id])
935
entry.apply(path, conflict_handler, reverse)
936
temp_name[entry.id] = None
938
elif entry.needs_rename():
939
to_name = os.path.join(temp_dir, str(i))
940
src_path = inventory.get(entry.id)
941
if src_path is not None:
942
src_path = os.path.join(dir, src_path)
944
rename(src_path, to_name)
945
temp_name[entry.id] = to_name
947
if e.errno != errno.ENOENT:
949
if conflict_handler.missing_for_rename(src_path, to_name) \
956
def rename_to_new_create(changed_inventory, target_entries, inventory,
957
changeset, dir, conflict_handler, reverse):
958
"""Rename entries with temp names to their final names, create new files.
960
:param changed_inventory: A mapping of id to temporary name
961
:type changed_inventory: Dictionary
962
:param target_entries: The entries to apply changes to
963
:type target_entries: List of `ChangesetEntry`
964
:param changeset: The changeset to apply
965
:type changeset: `Changeset`
966
:param dir: The directory to apply changes to
968
:param reverse: If true, apply changes in reverse
971
for entry in target_entries:
972
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
973
if new_tree_path is None:
975
new_path = os.path.join(dir, new_tree_path)
976
old_path = changed_inventory.get(entry.id)
977
if bzrlib.osutils.lexists(new_path):
978
if conflict_handler.target_exists(entry, new_path, old_path) == \
981
if entry.is_creation(reverse):
982
entry.apply(new_path, conflict_handler, reverse)
983
changed_inventory[entry.id] = new_tree_path
984
elif entry.needs_rename():
988
rename(old_path, new_path)
989
changed_inventory[entry.id] = new_tree_path
991
raise Exception ("%s is missing" % new_path)
993
class TargetExists(Exception):
994
def __init__(self, entry, target):
995
msg = "The path %s already exists" % target
996
Exception.__init__(self, msg)
1000
class RenameConflict(Exception):
1001
def __init__(self, id, this_name, base_name, other_name):
1002
msg = """Trees all have different names for a file
1006
id: %s""" % (this_name, base_name, other_name, id)
1007
Exception.__init__(self, msg)
1008
self.this_name = this_name
1009
self.base_name = base_name
1010
self_other_name = other_name
1012
class MoveConflict(Exception):
1013
def __init__(self, id, this_parent, base_parent, other_parent):
1014
msg = """The file is in different directories in every tree
1018
id: %s""" % (this_parent, base_parent, other_parent, id)
1019
Exception.__init__(self, msg)
1020
self.this_parent = this_parent
1021
self.base_parent = base_parent
1022
self_other_parent = other_parent
1024
class MergeConflict(Exception):
1025
def __init__(self, this_path):
1026
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1027
self.this_path = this_path
1029
class WrongOldContents(Exception):
1030
def __init__(self, filename):
1031
msg = "Contents mismatch deleting %s" % filename
1032
self.filename = filename
1033
Exception.__init__(self, msg)
1035
class WrongOldExecFlag(Exception):
1036
def __init__(self, filename, old_exec_flag, new_exec_flag):
1037
msg = "Executable flag missmatch on %s:\n" \
1038
"Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
1039
self.filename = filename
1040
Exception.__init__(self, msg)
1042
class RemoveContentsConflict(Exception):
1043
def __init__(self, filename):
1044
msg = "Conflict deleting %s, which has different contents in BASE"\
1045
" and THIS" % filename
1046
self.filename = filename
1047
Exception.__init__(self, msg)
1049
class DeletingNonEmptyDirectory(Exception):
1050
def __init__(self, filename):
1051
msg = "Trying to remove dir %s while it still had files" % filename
1052
self.filename = filename
1053
Exception.__init__(self, msg)
1056
class PatchTargetMissing(Exception):
1057
def __init__(self, filename):
1058
msg = "Attempt to patch %s, which does not exist" % filename
1059
Exception.__init__(self, msg)
1060
self.filename = filename
1062
class MissingForSetExec(Exception):
1063
def __init__(self, filename):
1064
msg = "Attempt to change permissions on %s, which does not exist" %\
1066
Exception.__init__(self, msg)
1067
self.filename = filename
1069
class MissingForRm(Exception):
1070
def __init__(self, filename):
1071
msg = "Attempt to remove missing path %s" % filename
1072
Exception.__init__(self, msg)
1073
self.filename = filename
1076
class MissingForRename(Exception):
1077
def __init__(self, filename, to_path):
1078
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1079
Exception.__init__(self, msg)
1080
self.filename = filename
1082
class NewContentsConflict(Exception):
1083
def __init__(self, filename):
1084
msg = "Conflicting contents for new file %s" % (filename)
1085
Exception.__init__(self, msg)
1087
class WeaveMergeConflict(Exception):
1088
def __init__(self, filename):
1089
msg = "Conflicting contents for file %s" % (filename)
1090
Exception.__init__(self, msg)
1092
class ThreewayContentsConflict(Exception):
1093
def __init__(self, filename):
1094
msg = "Conflicting contents for file %s" % (filename)
1095
Exception.__init__(self, msg)
1098
class MissingForMerge(Exception):
1099
def __init__(self, filename):
1100
msg = "The file %s was modified, but does not exist in this tree"\
1102
Exception.__init__(self, msg)
1105
class ExceptionConflictHandler(object):
1106
"""Default handler for merge exceptions.
1108
This throws an error on any kind of conflict. Conflict handlers can
1109
descend from this class if they have a better way to handle some or
1110
all types of conflict.
1112
def missing_parent(self, pathname):
1113
parent = os.path.dirname(pathname)
1114
raise Exception("Parent directory missing for %s" % pathname)
1116
def dir_exists(self, pathname):
1117
raise Exception("Directory already exists for %s" % pathname)
1119
def failed_hunks(self, pathname):
1120
raise Exception("Failed to apply some hunks for %s" % pathname)
1122
def target_exists(self, entry, target, old_path):
1123
raise TargetExists(entry, target)
1125
def rename_conflict(self, id, this_name, base_name, other_name):
1126
raise RenameConflict(id, this_name, base_name, other_name)
1128
def move_conflict(self, id, this_dir, base_dir, other_dir):
1129
raise MoveConflict(id, this_dir, base_dir, other_dir)
1131
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1133
raise MergeConflict(this_path)
1135
def wrong_old_contents(self, filename, expected_contents):
1136
raise WrongOldContents(filename)
1138
def rem_contents_conflict(self, filename, this_contents, base_contents):
1139
raise RemoveContentsConflict(filename)
1141
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1142
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1144
def rmdir_non_empty(self, filename):
1145
raise DeletingNonEmptyDirectory(filename)
1147
def link_name_exists(self, filename):
1148
raise TargetExists(filename)
1150
def patch_target_missing(self, filename, contents):
1151
raise PatchTargetMissing(filename)
1153
def missing_for_exec_flag(self, filename):
1154
raise MissingForExecFlag(filename)
1156
def missing_for_rm(self, filename, change):
1157
raise MissingForRm(filename)
1159
def missing_for_rename(self, filename, to_path):
1160
raise MissingForRename(filename, to_path)
1162
def missing_for_merge(self, file_id, other_path):
1163
raise MissingForMerge(other_path)
1165
def new_contents_conflict(self, filename, other_contents):
1166
raise NewContentsConflict(filename)
1168
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1169
raise WeaveMergeConflict(filename)
1171
def threeway_contents_conflict(self, filename, this_contents,
1172
base_contents, other_contents):
1173
raise ThreewayContentsConflict(filename)
1178
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1180
"""Apply a changeset to a directory.
1182
:param changeset: The changes to perform
1183
:type changeset: `Changeset`
1184
:param inventory: The mapping of id to filename for the directory
1185
:type inventory: Dictionary
1186
:param dir: The path of the directory to apply the changes to
1188
:param reverse: If true, apply the changes in reverse
1190
:return: The mapping of the changed entries
1193
if conflict_handler is None:
1194
conflict_handler = ExceptionConflictHandler()
1195
temp_dir = os.path.join(dir, "bzr-tree-change")
1199
if e.errno == errno.EEXIST:
1203
if e.errno == errno.ENOTEMPTY:
1204
raise OldFailedTreeOp()
1209
#apply changes that don't affect filenames
1210
for entry in changeset.entries.itervalues():
1211
if not entry.is_creation_or_deletion() and not entry.is_boring():
1212
if entry.id not in inventory:
1213
warning("entry {%s} no longer present, can't be updated",
1216
path = os.path.join(dir, inventory[entry.id])
1217
entry.apply(path, conflict_handler, reverse)
1219
# Apply renames in stages, to minimize conflicts:
1220
# Only files whose name or parent change are interesting, because their
1221
# target name may exist in the source tree. If a directory's name changes,
1222
# that doesn't make its children interesting.
1223
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1226
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1227
temp_dir, conflict_handler,
1230
rename_to_new_create(changed_inventory, target_entries, inventory,
1231
changeset, dir, conflict_handler, reverse)
1233
return changed_inventory
1236
def apply_changeset_tree(cset, tree, reverse=False):
1238
for entry in tree.source_inventory().itervalues():
1239
inventory[entry.id] = entry.path
1240
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1242
new_entries, remove_entries = \
1243
get_inventory_change(inventory, new_inventory, cset, reverse)
1244
tree.update_source_inventory(new_entries, remove_entries)
1247
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1250
for entry in cset.entries.itervalues():
1251
if entry.needs_rename():
1252
new_path = entry.get_new_path(inventory, cset)
1253
if new_path is None:
1254
remove_entries.append(entry.id)
1256
new_entries[new_path] = entry.id
1257
return new_entries, remove_entries
1260
def print_changeset(cset):
1261
"""Print all non-boring changeset entries
1263
:param cset: The changeset to print
1264
:type cset: `Changeset`
1266
for entry in cset.entries.itervalues():
1267
if entry.is_boring():
1270
print entry.summarize_name(cset)
1272
class CompositionFailure(Exception):
1273
def __init__(self, old_entry, new_entry, problem):
1274
msg = "Unable to conpose entries.\n %s" % problem
1275
Exception.__init__(self, msg)
1277
class IDMismatch(CompositionFailure):
1278
def __init__(self, old_entry, new_entry):
1279
problem = "Attempt to compose entries with different ids: %s and %s" %\
1280
(old_entry.id, new_entry.id)
1281
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1283
def compose_changesets(old_cset, new_cset):
1284
"""Combine two changesets into one. This works well for exact patching.
1285
Otherwise, not so well.
1287
:param old_cset: The first changeset that would be applied
1288
:type old_cset: `Changeset`
1289
:param new_cset: The second changeset that would be applied
1290
:type new_cset: `Changeset`
1291
:return: A changeset that combines the changes in both changesets
1294
composed = Changeset()
1295
for old_entry in old_cset.entries.itervalues():
1296
new_entry = new_cset.entries.get(old_entry.id)
1297
if new_entry is None:
1298
composed.add_entry(old_entry)
1300
composed_entry = compose_entries(old_entry, new_entry)
1301
if composed_entry.parent is not None or\
1302
composed_entry.new_parent is not None:
1303
composed.add_entry(composed_entry)
1304
for new_entry in new_cset.entries.itervalues():
1305
if not old_cset.entries.has_key(new_entry.id):
1306
composed.add_entry(new_entry)
1309
def compose_entries(old_entry, new_entry):
1310
"""Combine two entries into one.
1312
:param old_entry: The first entry that would be applied
1313
:type old_entry: ChangesetEntry
1314
:param old_entry: The second entry that would be applied
1315
:type old_entry: ChangesetEntry
1316
:return: A changeset entry combining both entries
1317
:rtype: `ChangesetEntry`
1319
if old_entry.id != new_entry.id:
1320
raise IDMismatch(old_entry, new_entry)
1321
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1323
if (old_entry.parent != old_entry.new_parent or
1324
new_entry.parent != new_entry.new_parent):
1325
output.new_parent = new_entry.new_parent
1327
if (old_entry.path != old_entry.new_path or
1328
new_entry.path != new_entry.new_path):
1329
output.new_path = new_entry.new_path
1331
output.contents_change = compose_contents(old_entry, new_entry)
1332
output.metadata_change = compose_metadata(old_entry, new_entry)
1335
def compose_contents(old_entry, new_entry):
1336
"""Combine the contents of two changeset entries. Entries are combined
1337
intelligently where possible, but the fallback behavior returns an
1340
:param old_entry: The first entry that would be applied
1341
:type old_entry: `ChangesetEntry`
1342
:param new_entry: The second entry that would be applied
1343
:type new_entry: `ChangesetEntry`
1344
:return: A combined contents change
1345
:rtype: anything supporting the apply(reverse=False) method
1347
old_contents = old_entry.contents_change
1348
new_contents = new_entry.contents_change
1349
if old_entry.contents_change is None:
1350
return new_entry.contents_change
1351
elif new_entry.contents_change is None:
1352
return old_entry.contents_change
1353
elif isinstance(old_contents, ReplaceContents) and \
1354
isinstance(new_contents, ReplaceContents):
1355
if old_contents.old_contents == new_contents.new_contents:
1358
return ReplaceContents(old_contents.old_contents,
1359
new_contents.new_contents)
1360
elif isinstance(old_contents, ApplySequence):
1361
output = ApplySequence(old_contents.changes)
1362
if isinstance(new_contents, ApplySequence):
1363
output.changes.extend(new_contents.changes)
1365
output.changes.append(new_contents)
1367
elif isinstance(new_contents, ApplySequence):
1368
output = ApplySequence((old_contents.changes,))
1369
output.extend(new_contents.changes)
1372
return ApplySequence((old_contents, new_contents))
1374
def compose_metadata(old_entry, new_entry):
1375
old_meta = old_entry.metadata_change
1376
new_meta = new_entry.metadata_change
1377
if old_meta is None:
1379
elif new_meta is None:
1381
elif (isinstance(old_meta, ChangeExecFlag) and
1382
isinstance(new_meta, ChangeExecFlag)):
1383
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1385
return ApplySequence(old_meta, new_meta)
1388
def changeset_is_null(changeset):
1389
for entry in changeset.entries.itervalues():
1390
if not entry.is_boring():
1394
class UnsupportedFiletype(Exception):
1395
def __init__(self, kind, full_path):
1396
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1398
Exception.__init__(self, msg)
1399
self.full_path = full_path
1402
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1403
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1406
class ChangesetGenerator(object):
1407
def __init__(self, tree_a, tree_b, interesting_ids=None):
1408
object.__init__(self)
1409
self.tree_a = tree_a
1410
self.tree_b = tree_b
1411
self._interesting_ids = interesting_ids
1413
def iter_both_tree_ids(self):
1414
for file_id in self.tree_a:
1416
for file_id in self.tree_b:
1417
if file_id not in self.tree_a:
1422
for file_id in self.iter_both_tree_ids():
1423
cs_entry = self.make_entry(file_id)
1424
if cs_entry is not None and not cs_entry.is_boring():
1425
cset.add_entry(cs_entry)
1427
for entry in list(cset.entries.itervalues()):
1428
if entry.parent != entry.new_parent:
1429
if not cset.entries.has_key(entry.parent) and\
1430
entry.parent != NULL_ID and entry.parent is not None:
1431
parent_entry = self.make_boring_entry(entry.parent)
1432
cset.add_entry(parent_entry)
1433
if not cset.entries.has_key(entry.new_parent) and\
1434
entry.new_parent != NULL_ID and \
1435
entry.new_parent is not None:
1436
parent_entry = self.make_boring_entry(entry.new_parent)
1437
cset.add_entry(parent_entry)
1440
def iter_inventory(self, tree):
1441
for file_id in tree:
1442
yield self.get_entry(file_id, tree)
1444
def get_entry(self, file_id, tree):
1445
if not tree.has_or_had_id(file_id):
1447
return tree.inventory[file_id]
1449
def get_entry_parent(self, entry):
1452
return entry.parent_id
1454
def get_path(self, file_id, tree):
1455
if not tree.has_or_had_id(file_id):
1457
path = tree.id2path(file_id)
1463
def make_basic_entry(self, file_id, only_interesting):
1464
entry_a = self.get_entry(file_id, self.tree_a)
1465
entry_b = self.get_entry(file_id, self.tree_b)
1466
if only_interesting and not self.is_interesting(entry_a, entry_b):
1468
parent = self.get_entry_parent(entry_a)
1469
path = self.get_path(file_id, self.tree_a)
1470
cs_entry = ChangesetEntry(file_id, parent, path)
1471
new_parent = self.get_entry_parent(entry_b)
1473
new_path = self.get_path(file_id, self.tree_b)
1475
cs_entry.new_path = new_path
1476
cs_entry.new_parent = new_parent
1479
def is_interesting(self, entry_a, entry_b):
1480
if self._interesting_ids is None:
1482
if entry_a is not None:
1483
file_id = entry_a.file_id
1484
elif entry_b is not None:
1485
file_id = entry_b.file_id
1488
return file_id in self._interesting_ids
1490
def make_boring_entry(self, id):
1491
cs_entry = self.make_basic_entry(id, only_interesting=False)
1492
if cs_entry.is_creation_or_deletion():
1493
return self.make_entry(id, only_interesting=False)
1498
def make_entry(self, id, only_interesting=True):
1499
cs_entry = self.make_basic_entry(id, only_interesting)
1501
if cs_entry is None:
1504
cs_entry.metadata_change = self.make_exec_flag_change(id)
1506
if id in self.tree_a and id in self.tree_b:
1507
a_sha1 = self.tree_a.get_file_sha1(id)
1508
b_sha1 = self.tree_b.get_file_sha1(id)
1509
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1512
cs_entry.contents_change = self.make_contents_change(id)
1515
def make_exec_flag_change(self, file_id):
1516
exec_flag_a = exec_flag_b = None
1517
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1518
exec_flag_a = self.tree_a.is_executable(file_id)
1520
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1521
exec_flag_b = self.tree_b.is_executable(file_id)
1523
if exec_flag_a == exec_flag_b:
1525
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1527
def make_contents_change(self, file_id):
1528
a_contents = get_contents(self.tree_a, file_id)
1529
b_contents = get_contents(self.tree_b, file_id)
1530
if a_contents == b_contents:
1532
return ReplaceContents(a_contents, b_contents)
1535
def get_contents(tree, file_id):
1536
"""Return the appropriate contents to create a copy of file_id from tree"""
1537
if file_id not in tree:
1539
kind = tree.kind(file_id)
1541
return TreeFileCreate(tree, file_id)
1542
elif kind in ("directory", "root_directory"):
1544
elif kind == "symlink":
1545
return SymlinkCreate(tree.get_symlink_target(file_id))
1547
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1550
def full_path(entry, tree):
1551
return os.path.join(tree.basedir, entry.path)
1553
def new_delete_entry(entry, tree, inventory, delete):
1554
if entry.path == "":
1557
parent = inventory[dirname(entry.path)].id
1558
cs_entry = ChangesetEntry(parent, entry.path)
1560
cs_entry.new_path = None
1561
cs_entry.new_parent = None
1563
cs_entry.path = None
1564
cs_entry.parent = None
1565
full_path = full_path(entry, tree)
1566
status = os.lstat(full_path)
1567
if stat.S_ISDIR(file_stat.st_mode):
1573
# XXX: Can't we unify this with the regular inventory object
1574
class Inventory(object):
1575
def __init__(self, inventory):
1576
self.inventory = inventory
1577
self.rinventory = None
1579
def get_rinventory(self):
1580
if self.rinventory is None:
1581
self.rinventory = invert_dict(self.inventory)
1582
return self.rinventory
1584
def get_path(self, id):
1585
return self.inventory.get(id)
1587
def get_name(self, id):
1588
path = self.get_path(id)
1592
return os.path.basename(path)
1594
def get_dir(self, id):
1595
path = self.get_path(id)
1600
return os.path.dirname(path)
1602
def get_parent(self, id):
1603
if self.get_path(id) is None:
1605
directory = self.get_dir(id)
1606
if directory == '.':
1608
if directory is None:
1610
return self.get_rinventory().get(directory)