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
def __init__(self, file_id, base, other):
415
self.file_id = file_id
419
def is_creation(self):
422
def is_deletion(self):
425
def __eq__(self, other):
426
if not isinstance(other, Diff3Merge):
428
return (self.base == other.base and
429
self.other == other.other and self.file_id == other.file_id)
431
def __ne__(self, other):
432
return not (self == other)
434
def dump_file(self, temp_dir, name, tree):
435
out_path = os.path.join(temp_dir, name)
436
out_file = file(out_path, "wb")
437
in_file = tree.get_file(self.file_id)
442
def apply(self, filename, conflict_handler, reverse=False):
443
temp_dir = mkdtemp(prefix="bzr-")
445
new_file = filename+".new"
446
base_file = self.dump_file(temp_dir, "base", self.base)
447
other_file = self.dump_file(temp_dir, "other", self.other)
454
status = patch.diff3(new_file, filename, base, other)
456
os.chmod(new_file, os.stat(filename).st_mode)
457
rename(new_file, filename)
461
def get_lines(filename):
462
my_file = file(filename, "rb")
463
lines = my_file.readlines()
466
base_lines = get_lines(base)
467
other_lines = get_lines(other)
468
conflict_handler.merge_conflict(new_file, filename, base_lines,
475
"""Convenience function to create a directory.
477
:return: A ReplaceContents that will create a directory
478
:rtype: `ReplaceContents`
480
return ReplaceContents(None, dir_create)
483
"""Convenience function to delete a directory.
485
:return: A ReplaceContents that will delete a directory
486
:rtype: `ReplaceContents`
488
return ReplaceContents(dir_create, None)
490
def CreateFile(contents):
491
"""Convenience fucntion to create a file.
493
:param contents: The contents of the file to create
495
:return: A ReplaceContents that will create a file
496
:rtype: `ReplaceContents`
498
return ReplaceContents(None, FileCreate(contents))
500
def DeleteFile(contents):
501
"""Convenience fucntion to delete a file.
503
:param contents: The contents of the file to delete
505
:return: A ReplaceContents that will delete a file
506
:rtype: `ReplaceContents`
508
return ReplaceContents(FileCreate(contents), None)
510
def ReplaceFileContents(old_tree, new_tree, file_id):
511
"""Convenience fucntion to replace the contents of a file.
513
:param old_contents: The contents of the file to replace
514
:type old_contents: str
515
:param new_contents: The contents to replace the file with
516
:type new_contents: str
517
:return: A ReplaceContents that will replace the contents of a file a file
518
:rtype: `ReplaceContents`
520
return ReplaceContents(TreeFileCreate(old_tree, file_id),
521
TreeFileCreate(new_tree, file_id))
523
def CreateSymlink(target):
524
"""Convenience fucntion to create a symlink.
526
:param target: The path the link should point to
528
:return: A ReplaceContents that will delete a file
529
:rtype: `ReplaceContents`
531
return ReplaceContents(None, SymlinkCreate(target))
533
def DeleteSymlink(target):
534
"""Convenience fucntion to delete a symlink.
536
:param target: The path the link should point to
538
:return: A ReplaceContents that will delete a file
539
:rtype: `ReplaceContents`
541
return ReplaceContents(SymlinkCreate(target), None)
543
def ChangeTarget(old_target, new_target):
544
"""Convenience fucntion to change the target of a symlink.
546
:param old_target: The current link target
547
:type old_target: str
548
:param new_target: The new link target to use
549
:type new_target: str
550
:return: A ReplaceContents that will delete a file
551
:rtype: `ReplaceContents`
553
return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
556
class InvalidEntry(Exception):
557
"""Raise when a ChangesetEntry is invalid in some way"""
558
def __init__(self, entry, problem):
561
:param entry: The invalid ChangesetEntry
562
:type entry: `ChangesetEntry`
563
:param problem: The problem with the entry
566
msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id,
569
Exception.__init__(self, msg)
573
class SourceRootHasName(InvalidEntry):
574
"""This changeset entry has a name other than "", but its parent is !NULL"""
575
def __init__(self, entry, name):
578
:param entry: The invalid ChangesetEntry
579
:type entry: `ChangesetEntry`
580
:param name: The name of the entry
583
msg = 'Child of !NULL is named "%s", not "./.".' % name
584
InvalidEntry.__init__(self, entry, msg)
586
class NullIDAssigned(InvalidEntry):
587
"""The id !NULL was assigned to a real entry"""
588
def __init__(self, entry):
591
:param entry: The invalid ChangesetEntry
592
:type entry: `ChangesetEntry`
594
msg = '"!NULL" id assigned to a file "%s".' % entry.path
595
InvalidEntry.__init__(self, entry, msg)
597
class ParentIDIsSelf(InvalidEntry):
598
"""An entry is marked as its own parent"""
599
def __init__(self, entry):
602
:param entry: The invalid ChangesetEntry
603
:type entry: `ChangesetEntry`
605
msg = 'file %s has "%s" id for both self id and parent id.' % \
606
(entry.path, entry.id)
607
InvalidEntry.__init__(self, entry, msg)
609
class ChangesetEntry(object):
610
"""An entry the changeset"""
611
def __init__(self, id, parent, path):
612
"""Constructor. Sets parent and name assuming it was not
613
renamed/created/deleted.
614
:param id: The id associated with the entry
615
:param parent: The id of the parent of this entry (or !NULL if no
617
:param path: The file path relative to the tree root of this entry
623
self.new_parent = parent
624
self.contents_change = None
625
self.metadata_change = None
626
if parent == NULL_ID and path !='./.':
627
raise SourceRootHasName(self, path)
628
if self.id == NULL_ID:
629
raise NullIDAssigned(self)
630
if self.id == self.parent:
631
raise ParentIDIsSelf(self)
634
return "ChangesetEntry(%s)" % self.id
637
if self.path is None:
639
return os.path.dirname(self.path)
641
def __set_dir(self, dir):
642
self.path = os.path.join(dir, os.path.basename(self.path))
644
dir = property(__get_dir, __set_dir)
646
def __get_name(self):
647
if self.path is None:
649
return os.path.basename(self.path)
651
def __set_name(self, name):
652
self.path = os.path.join(os.path.dirname(self.path), name)
654
name = property(__get_name, __set_name)
656
def __get_new_dir(self):
657
if self.new_path is None:
659
return os.path.dirname(self.new_path)
661
def __set_new_dir(self, dir):
662
self.new_path = os.path.join(dir, os.path.basename(self.new_path))
664
new_dir = property(__get_new_dir, __set_new_dir)
666
def __get_new_name(self):
667
if self.new_path is None:
669
return os.path.basename(self.new_path)
671
def __set_new_name(self, name):
672
self.new_path = os.path.join(os.path.dirname(self.new_path), name)
674
new_name = property(__get_new_name, __set_new_name)
676
def needs_rename(self):
677
"""Determines whether the entry requires renaming.
682
return (self.parent != self.new_parent or self.name != self.new_name)
684
def is_deletion(self, reverse):
685
"""Return true if applying the entry would delete a file/directory.
687
:param reverse: if true, the changeset is being applied in reverse
690
return self.is_creation(not reverse)
692
def is_creation(self, reverse):
693
"""Return true if applying the entry would create a file/directory.
695
:param reverse: if true, the changeset is being applied in reverse
698
if self.contents_change is None:
701
return self.contents_change.is_deletion()
703
return self.contents_change.is_creation()
705
def is_creation_or_deletion(self):
706
"""Return true if applying the entry would create or delete a
711
return self.is_creation(False) or self.is_deletion(False)
713
def get_cset_path(self, mod=False):
714
"""Determine the path of the entry according to the changeset.
716
:param changeset: The changeset to derive the path from
717
:type changeset: `Changeset`
718
:param mod: If true, generate the MOD path. Otherwise, generate the \
720
:return: the path of the entry, or None if it did not exist in the \
722
:rtype: str or NoneType
725
if self.new_parent == NULL_ID:
727
elif self.new_parent is None:
731
if self.parent == NULL_ID:
733
elif self.parent is None:
737
def summarize_name(self, reverse=False):
738
"""Produce a one-line summary of the filename. Indicates renames as
739
old => new, indicates creation as None => new, indicates deletion as
742
:param changeset: The changeset to get paths from
743
:type changeset: `Changeset`
744
:param reverse: If true, reverse the names in the output
748
orig_path = self.get_cset_path(False)
749
mod_path = self.get_cset_path(True)
750
if orig_path is not None:
751
orig_path = orig_path[2:]
752
if mod_path is not None:
753
mod_path = mod_path[2:]
754
if orig_path == mod_path:
758
return "%s => %s" % (orig_path, mod_path)
760
return "%s => %s" % (mod_path, orig_path)
763
def get_new_path(self, id_map, changeset, reverse=False):
764
"""Determine the full pathname to rename to
766
:param id_map: The map of ids to filenames for the tree
767
:type id_map: Dictionary
768
:param changeset: The changeset to get data from
769
:type changeset: `Changeset`
770
:param reverse: If true, we're applying the changeset in reverse
774
mutter("Finding new path for %s" % self.summarize_name())
778
from_dir = self.new_dir
780
from_name = self.new_name
782
parent = self.new_parent
783
to_dir = self.new_dir
785
to_name = self.new_name
786
from_name = self.name
791
if parent == NULL_ID or parent is None:
793
raise SourceRootHasName(self, to_name)
796
if from_dir == to_dir:
797
dir = os.path.dirname(id_map[self.id])
799
mutter("path, new_path: %r %r" % (self.path, self.new_path))
800
parent_entry = changeset.entries[parent]
801
dir = parent_entry.get_new_path(id_map, changeset, reverse)
802
if from_name == to_name:
803
name = os.path.basename(id_map[self.id])
806
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
807
return os.path.join(dir, name)
810
"""Determines whether the entry does nothing
812
:return: True if the entry does no renames or content changes
815
if self.contents_change is not None:
817
elif self.metadata_change is not None:
819
elif self.parent != self.new_parent:
821
elif self.name != self.new_name:
826
def apply(self, filename, conflict_handler, reverse=False):
827
"""Applies the file content and/or metadata changes.
829
:param filename: the filename of the entry
831
:param reverse: If true, apply the changes in reverse
834
if self.is_deletion(reverse) and self.metadata_change is not None:
835
self.metadata_change.apply(filename, conflict_handler, reverse)
836
if self.contents_change is not None:
837
self.contents_change.apply(filename, conflict_handler, reverse)
838
if not self.is_deletion(reverse) and self.metadata_change is not None:
839
self.metadata_change.apply(filename, conflict_handler, reverse)
841
class IDPresent(Exception):
842
def __init__(self, id):
843
msg = "Cannot add entry because that id has already been used:\n%s" %\
845
Exception.__init__(self, msg)
848
class Changeset(object):
849
"""A set of changes to apply"""
853
def add_entry(self, entry):
854
"""Add an entry to the list of entries"""
855
if self.entries.has_key(entry.id):
856
raise IDPresent(entry.id)
857
self.entries[entry.id] = entry
859
def my_sort(sequence, key, reverse=False):
860
"""A sort function that supports supplying a key for comparison
862
:param sequence: The sequence to sort
863
:param key: A callable object that returns the values to be compared
864
:param reverse: If true, sort in reverse order
867
def cmp_by_key(entry_a, entry_b):
872
return cmp(key(entry_a), key(entry_b))
873
sequence.sort(cmp_by_key)
875
def get_rename_entries(changeset, inventory, reverse):
876
"""Return a list of entries that will be renamed. Entries are sorted from
877
longest to shortest source path and from shortest to longest target path.
879
:param changeset: The changeset to look in
880
:type changeset: `Changeset`
881
:param inventory: The source of current tree paths for the given ids
882
:type inventory: Dictionary
883
:param reverse: If true, the changeset is being applied in reverse
885
:return: source entries and target entries as a tuple
888
source_entries = [x for x in changeset.entries.itervalues()
889
if x.needs_rename() or x.is_creation_or_deletion()]
890
# these are done from longest path to shortest, to avoid deleting a
891
# parent before its children are deleted/renamed
892
def longest_to_shortest(entry):
893
path = inventory.get(entry.id)
898
my_sort(source_entries, longest_to_shortest, reverse=True)
900
target_entries = source_entries[:]
901
# These are done from shortest to longest path, to avoid creating a
902
# child before its parent has been created/renamed
903
def shortest_to_longest(entry):
904
path = entry.get_new_path(inventory, changeset, reverse)
909
my_sort(target_entries, shortest_to_longest)
910
return (source_entries, target_entries)
912
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
913
conflict_handler, reverse):
914
"""Delete and rename entries as appropriate. Entries are renamed to temp
915
names. A map of id -> temp name (or None, for deletions) is returned.
917
:param source_entries: The entries to rename and delete
918
:type source_entries: List of `ChangesetEntry`
919
:param inventory: The map of id -> filename in the current tree
920
:type inventory: Dictionary
921
:param dir: The directory to apply changes to
923
:param reverse: Apply changes in reverse
925
:return: a mapping of id to temporary name
929
for i in range(len(source_entries)):
930
entry = source_entries[i]
931
if entry.is_deletion(reverse):
932
path = os.path.join(dir, inventory[entry.id])
933
entry.apply(path, conflict_handler, reverse)
934
temp_name[entry.id] = None
936
elif entry.needs_rename():
937
to_name = os.path.join(temp_dir, str(i))
938
src_path = inventory.get(entry.id)
939
if src_path is not None:
940
src_path = os.path.join(dir, src_path)
942
rename(src_path, to_name)
943
temp_name[entry.id] = to_name
945
if e.errno != errno.ENOENT:
947
if conflict_handler.missing_for_rename(src_path, to_name) \
954
def rename_to_new_create(changed_inventory, target_entries, inventory,
955
changeset, dir, conflict_handler, reverse):
956
"""Rename entries with temp names to their final names, create new files.
958
:param changed_inventory: A mapping of id to temporary name
959
:type changed_inventory: Dictionary
960
:param target_entries: The entries to apply changes to
961
:type target_entries: List of `ChangesetEntry`
962
:param changeset: The changeset to apply
963
:type changeset: `Changeset`
964
:param dir: The directory to apply changes to
966
:param reverse: If true, apply changes in reverse
969
for entry in target_entries:
970
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
971
if new_tree_path is None:
973
new_path = os.path.join(dir, new_tree_path)
974
old_path = changed_inventory.get(entry.id)
975
if bzrlib.osutils.lexists(new_path):
976
if conflict_handler.target_exists(entry, new_path, old_path) == \
979
if entry.is_creation(reverse):
980
entry.apply(new_path, conflict_handler, reverse)
981
changed_inventory[entry.id] = new_tree_path
982
elif entry.needs_rename():
986
rename(old_path, new_path)
987
changed_inventory[entry.id] = new_tree_path
989
raise Exception ("%s is missing" % new_path)
991
class TargetExists(Exception):
992
def __init__(self, entry, target):
993
msg = "The path %s already exists" % target
994
Exception.__init__(self, msg)
998
class RenameConflict(Exception):
999
def __init__(self, id, this_name, base_name, other_name):
1000
msg = """Trees all have different names for a file
1004
id: %s""" % (this_name, base_name, other_name, id)
1005
Exception.__init__(self, msg)
1006
self.this_name = this_name
1007
self.base_name = base_name
1008
self_other_name = other_name
1010
class MoveConflict(Exception):
1011
def __init__(self, id, this_parent, base_parent, other_parent):
1012
msg = """The file is in different directories in every tree
1016
id: %s""" % (this_parent, base_parent, other_parent, id)
1017
Exception.__init__(self, msg)
1018
self.this_parent = this_parent
1019
self.base_parent = base_parent
1020
self_other_parent = other_parent
1022
class MergeConflict(Exception):
1023
def __init__(self, this_path):
1024
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1025
self.this_path = this_path
1027
class WrongOldContents(Exception):
1028
def __init__(self, filename):
1029
msg = "Contents mismatch deleting %s" % filename
1030
self.filename = filename
1031
Exception.__init__(self, msg)
1033
class WrongOldExecFlag(Exception):
1034
def __init__(self, filename, old_exec_flag, new_exec_flag):
1035
msg = "Executable flag missmatch on %s:\n" \
1036
"Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
1037
self.filename = filename
1038
Exception.__init__(self, msg)
1040
class RemoveContentsConflict(Exception):
1041
def __init__(self, filename):
1042
msg = "Conflict deleting %s, which has different contents in BASE"\
1043
" and THIS" % filename
1044
self.filename = filename
1045
Exception.__init__(self, msg)
1047
class DeletingNonEmptyDirectory(Exception):
1048
def __init__(self, filename):
1049
msg = "Trying to remove dir %s while it still had files" % filename
1050
self.filename = filename
1051
Exception.__init__(self, msg)
1054
class PatchTargetMissing(Exception):
1055
def __init__(self, filename):
1056
msg = "Attempt to patch %s, which does not exist" % filename
1057
Exception.__init__(self, msg)
1058
self.filename = filename
1060
class MissingForSetExec(Exception):
1061
def __init__(self, filename):
1062
msg = "Attempt to change permissions on %s, which does not exist" %\
1064
Exception.__init__(self, msg)
1065
self.filename = filename
1067
class MissingForRm(Exception):
1068
def __init__(self, filename):
1069
msg = "Attempt to remove missing path %s" % filename
1070
Exception.__init__(self, msg)
1071
self.filename = filename
1074
class MissingForRename(Exception):
1075
def __init__(self, filename, to_path):
1076
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1077
Exception.__init__(self, msg)
1078
self.filename = filename
1080
class NewContentsConflict(Exception):
1081
def __init__(self, filename):
1082
msg = "Conflicting contents for new file %s" % (filename)
1083
Exception.__init__(self, msg)
1085
class ThreewayContentsConflict(Exception):
1086
def __init__(self, filename):
1087
msg = "Conflicting contents for file %s" % (filename)
1088
Exception.__init__(self, msg)
1091
class MissingForMerge(Exception):
1092
def __init__(self, filename):
1093
msg = "The file %s was modified, but does not exist in this tree"\
1095
Exception.__init__(self, msg)
1098
class ExceptionConflictHandler(object):
1099
"""Default handler for merge exceptions.
1101
This throws an error on any kind of conflict. Conflict handlers can
1102
descend from this class if they have a better way to handle some or
1103
all types of conflict.
1105
def missing_parent(self, pathname):
1106
parent = os.path.dirname(pathname)
1107
raise Exception("Parent directory missing for %s" % pathname)
1109
def dir_exists(self, pathname):
1110
raise Exception("Directory already exists for %s" % pathname)
1112
def failed_hunks(self, pathname):
1113
raise Exception("Failed to apply some hunks for %s" % pathname)
1115
def target_exists(self, entry, target, old_path):
1116
raise TargetExists(entry, target)
1118
def rename_conflict(self, id, this_name, base_name, other_name):
1119
raise RenameConflict(id, this_name, base_name, other_name)
1121
def move_conflict(self, id, this_dir, base_dir, other_dir):
1122
raise MoveConflict(id, this_dir, base_dir, other_dir)
1124
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1126
raise MergeConflict(this_path)
1128
def wrong_old_contents(self, filename, expected_contents):
1129
raise WrongOldContents(filename)
1131
def rem_contents_conflict(self, filename, this_contents, base_contents):
1132
raise RemoveContentsConflict(filename)
1134
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1135
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1137
def rmdir_non_empty(self, filename):
1138
raise DeletingNonEmptyDirectory(filename)
1140
def link_name_exists(self, filename):
1141
raise TargetExists(filename)
1143
def patch_target_missing(self, filename, contents):
1144
raise PatchTargetMissing(filename)
1146
def missing_for_exec_flag(self, filename):
1147
raise MissingForExecFlag(filename)
1149
def missing_for_rm(self, filename, change):
1150
raise MissingForRm(filename)
1152
def missing_for_rename(self, filename, to_path):
1153
raise MissingForRename(filename, to_path)
1155
def missing_for_merge(self, file_id, other_path):
1156
raise MissingForMerge(other_path)
1158
def new_contents_conflict(self, filename, other_contents):
1159
raise NewContentsConflict(filename)
1161
def threeway_contents_conflict(self, filename, this_contents,
1162
base_contents, other_contents):
1163
raise ThreewayContentsConflict(filename)
1168
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1170
"""Apply a changeset to a directory.
1172
:param changeset: The changes to perform
1173
:type changeset: `Changeset`
1174
:param inventory: The mapping of id to filename for the directory
1175
:type inventory: Dictionary
1176
:param dir: The path of the directory to apply the changes to
1178
:param reverse: If true, apply the changes in reverse
1180
:return: The mapping of the changed entries
1183
if conflict_handler is None:
1184
conflict_handler = ExceptionConflictHandler()
1185
temp_dir = os.path.join(dir, "bzr-tree-change")
1189
if e.errno == errno.EEXIST:
1193
if e.errno == errno.ENOTEMPTY:
1194
raise OldFailedTreeOp()
1199
#apply changes that don't affect filenames
1200
for entry in changeset.entries.itervalues():
1201
if not entry.is_creation_or_deletion() and not entry.is_boring():
1202
path = os.path.join(dir, inventory[entry.id])
1203
entry.apply(path, conflict_handler, reverse)
1205
# Apply renames in stages, to minimize conflicts:
1206
# Only files whose name or parent change are interesting, because their
1207
# target name may exist in the source tree. If a directory's name changes,
1208
# that doesn't make its children interesting.
1209
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1212
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1213
temp_dir, conflict_handler,
1216
rename_to_new_create(changed_inventory, target_entries, inventory,
1217
changeset, dir, conflict_handler, reverse)
1219
return changed_inventory
1222
def apply_changeset_tree(cset, tree, reverse=False):
1224
for entry in tree.source_inventory().itervalues():
1225
inventory[entry.id] = entry.path
1226
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1228
new_entries, remove_entries = \
1229
get_inventory_change(inventory, new_inventory, cset, reverse)
1230
tree.update_source_inventory(new_entries, remove_entries)
1233
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1236
for entry in cset.entries.itervalues():
1237
if entry.needs_rename():
1238
new_path = entry.get_new_path(inventory, cset)
1239
if new_path is None:
1240
remove_entries.append(entry.id)
1242
new_entries[new_path] = entry.id
1243
return new_entries, remove_entries
1246
def print_changeset(cset):
1247
"""Print all non-boring changeset entries
1249
:param cset: The changeset to print
1250
:type cset: `Changeset`
1252
for entry in cset.entries.itervalues():
1253
if entry.is_boring():
1256
print entry.summarize_name(cset)
1258
class CompositionFailure(Exception):
1259
def __init__(self, old_entry, new_entry, problem):
1260
msg = "Unable to conpose entries.\n %s" % problem
1261
Exception.__init__(self, msg)
1263
class IDMismatch(CompositionFailure):
1264
def __init__(self, old_entry, new_entry):
1265
problem = "Attempt to compose entries with different ids: %s and %s" %\
1266
(old_entry.id, new_entry.id)
1267
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1269
def compose_changesets(old_cset, new_cset):
1270
"""Combine two changesets into one. This works well for exact patching.
1271
Otherwise, not so well.
1273
:param old_cset: The first changeset that would be applied
1274
:type old_cset: `Changeset`
1275
:param new_cset: The second changeset that would be applied
1276
:type new_cset: `Changeset`
1277
:return: A changeset that combines the changes in both changesets
1280
composed = Changeset()
1281
for old_entry in old_cset.entries.itervalues():
1282
new_entry = new_cset.entries.get(old_entry.id)
1283
if new_entry is None:
1284
composed.add_entry(old_entry)
1286
composed_entry = compose_entries(old_entry, new_entry)
1287
if composed_entry.parent is not None or\
1288
composed_entry.new_parent is not None:
1289
composed.add_entry(composed_entry)
1290
for new_entry in new_cset.entries.itervalues():
1291
if not old_cset.entries.has_key(new_entry.id):
1292
composed.add_entry(new_entry)
1295
def compose_entries(old_entry, new_entry):
1296
"""Combine two entries into one.
1298
:param old_entry: The first entry that would be applied
1299
:type old_entry: ChangesetEntry
1300
:param old_entry: The second entry that would be applied
1301
:type old_entry: ChangesetEntry
1302
:return: A changeset entry combining both entries
1303
:rtype: `ChangesetEntry`
1305
if old_entry.id != new_entry.id:
1306
raise IDMismatch(old_entry, new_entry)
1307
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1309
if (old_entry.parent != old_entry.new_parent or
1310
new_entry.parent != new_entry.new_parent):
1311
output.new_parent = new_entry.new_parent
1313
if (old_entry.path != old_entry.new_path or
1314
new_entry.path != new_entry.new_path):
1315
output.new_path = new_entry.new_path
1317
output.contents_change = compose_contents(old_entry, new_entry)
1318
output.metadata_change = compose_metadata(old_entry, new_entry)
1321
def compose_contents(old_entry, new_entry):
1322
"""Combine the contents of two changeset entries. Entries are combined
1323
intelligently where possible, but the fallback behavior returns an
1326
:param old_entry: The first entry that would be applied
1327
:type old_entry: `ChangesetEntry`
1328
:param new_entry: The second entry that would be applied
1329
:type new_entry: `ChangesetEntry`
1330
:return: A combined contents change
1331
:rtype: anything supporting the apply(reverse=False) method
1333
old_contents = old_entry.contents_change
1334
new_contents = new_entry.contents_change
1335
if old_entry.contents_change is None:
1336
return new_entry.contents_change
1337
elif new_entry.contents_change is None:
1338
return old_entry.contents_change
1339
elif isinstance(old_contents, ReplaceContents) and \
1340
isinstance(new_contents, ReplaceContents):
1341
if old_contents.old_contents == new_contents.new_contents:
1344
return ReplaceContents(old_contents.old_contents,
1345
new_contents.new_contents)
1346
elif isinstance(old_contents, ApplySequence):
1347
output = ApplySequence(old_contents.changes)
1348
if isinstance(new_contents, ApplySequence):
1349
output.changes.extend(new_contents.changes)
1351
output.changes.append(new_contents)
1353
elif isinstance(new_contents, ApplySequence):
1354
output = ApplySequence((old_contents.changes,))
1355
output.extend(new_contents.changes)
1358
return ApplySequence((old_contents, new_contents))
1360
def compose_metadata(old_entry, new_entry):
1361
old_meta = old_entry.metadata_change
1362
new_meta = new_entry.metadata_change
1363
if old_meta is None:
1365
elif new_meta is None:
1367
elif (isinstance(old_meta, ChangeExecFlag) and
1368
isinstance(new_meta, ChangeExecFlag)):
1369
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1371
return ApplySequence(old_meta, new_meta)
1374
def changeset_is_null(changeset):
1375
for entry in changeset.entries.itervalues():
1376
if not entry.is_boring():
1380
class UnsupportedFiletype(Exception):
1381
def __init__(self, kind, full_path):
1382
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1384
Exception.__init__(self, msg)
1385
self.full_path = full_path
1388
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1389
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1392
class ChangesetGenerator(object):
1393
def __init__(self, tree_a, tree_b, interesting_ids=None):
1394
object.__init__(self)
1395
self.tree_a = tree_a
1396
self.tree_b = tree_b
1397
self._interesting_ids = interesting_ids
1399
def iter_both_tree_ids(self):
1400
for file_id in self.tree_a:
1402
for file_id in self.tree_b:
1403
if file_id not in self.tree_a:
1408
for file_id in self.iter_both_tree_ids():
1409
cs_entry = self.make_entry(file_id)
1410
if cs_entry is not None and not cs_entry.is_boring():
1411
cset.add_entry(cs_entry)
1413
for entry in list(cset.entries.itervalues()):
1414
if entry.parent != entry.new_parent:
1415
if not cset.entries.has_key(entry.parent) and\
1416
entry.parent != NULL_ID and entry.parent is not None:
1417
parent_entry = self.make_boring_entry(entry.parent)
1418
cset.add_entry(parent_entry)
1419
if not cset.entries.has_key(entry.new_parent) and\
1420
entry.new_parent != NULL_ID and \
1421
entry.new_parent is not None:
1422
parent_entry = self.make_boring_entry(entry.new_parent)
1423
cset.add_entry(parent_entry)
1426
def iter_inventory(self, tree):
1427
for file_id in tree:
1428
yield self.get_entry(file_id, tree)
1430
def get_entry(self, file_id, tree):
1431
if not tree.has_or_had_id(file_id):
1433
return tree.inventory[file_id]
1435
def get_entry_parent(self, entry):
1438
return entry.parent_id
1440
def get_path(self, file_id, tree):
1441
if not tree.has_or_had_id(file_id):
1443
path = tree.id2path(file_id)
1449
def make_basic_entry(self, file_id, only_interesting):
1450
entry_a = self.get_entry(file_id, self.tree_a)
1451
entry_b = self.get_entry(file_id, self.tree_b)
1452
if only_interesting and not self.is_interesting(entry_a, entry_b):
1454
parent = self.get_entry_parent(entry_a)
1455
path = self.get_path(file_id, self.tree_a)
1456
cs_entry = ChangesetEntry(file_id, parent, path)
1457
new_parent = self.get_entry_parent(entry_b)
1459
new_path = self.get_path(file_id, self.tree_b)
1461
cs_entry.new_path = new_path
1462
cs_entry.new_parent = new_parent
1465
def is_interesting(self, entry_a, entry_b):
1466
if self._interesting_ids is None:
1468
if entry_a is not None:
1469
file_id = entry_a.file_id
1470
elif entry_b is not None:
1471
file_id = entry_b.file_id
1474
return file_id in self._interesting_ids
1476
def make_boring_entry(self, id):
1477
cs_entry = self.make_basic_entry(id, only_interesting=False)
1478
if cs_entry.is_creation_or_deletion():
1479
return self.make_entry(id, only_interesting=False)
1484
def make_entry(self, id, only_interesting=True):
1485
cs_entry = self.make_basic_entry(id, only_interesting)
1487
if cs_entry is None:
1490
cs_entry.metadata_change = self.make_exec_flag_change(id)
1492
if id in self.tree_a and id in self.tree_b:
1493
a_sha1 = self.tree_a.get_file_sha1(id)
1494
b_sha1 = self.tree_b.get_file_sha1(id)
1495
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1498
cs_entry.contents_change = self.make_contents_change(id)
1501
def make_exec_flag_change(self, file_id):
1502
exec_flag_a = exec_flag_b = None
1503
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1504
exec_flag_a = self.tree_a.is_executable(file_id)
1506
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1507
exec_flag_b = self.tree_b.is_executable(file_id)
1509
if exec_flag_a == exec_flag_b:
1511
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1513
def make_contents_change(self, file_id):
1514
a_contents = get_contents(self.tree_a, file_id)
1515
b_contents = get_contents(self.tree_b, file_id)
1516
if a_contents == b_contents:
1518
return ReplaceContents(a_contents, b_contents)
1521
def get_contents(tree, file_id):
1522
"""Return the appropriate contents to create a copy of file_id from tree"""
1523
if file_id not in tree:
1525
kind = tree.kind(file_id)
1527
return TreeFileCreate(tree, file_id)
1528
elif kind in ("directory", "root_directory"):
1530
elif kind == "symlink":
1531
return SymlinkCreate(tree.get_symlink_target(file_id))
1533
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1536
def full_path(entry, tree):
1537
return os.path.join(tree.basedir, entry.path)
1539
def new_delete_entry(entry, tree, inventory, delete):
1540
if entry.path == "":
1543
parent = inventory[dirname(entry.path)].id
1544
cs_entry = ChangesetEntry(parent, entry.path)
1546
cs_entry.new_path = None
1547
cs_entry.new_parent = None
1549
cs_entry.path = None
1550
cs_entry.parent = None
1551
full_path = full_path(entry, tree)
1552
status = os.lstat(full_path)
1553
if stat.S_ISDIR(file_stat.st_mode):
1559
# XXX: Can't we unify this with the regular inventory object
1560
class Inventory(object):
1561
def __init__(self, inventory):
1562
self.inventory = inventory
1563
self.rinventory = None
1565
def get_rinventory(self):
1566
if self.rinventory is None:
1567
self.rinventory = invert_dict(self.inventory)
1568
return self.rinventory
1570
def get_path(self, id):
1571
return self.inventory.get(id)
1573
def get_name(self, id):
1574
path = self.get_path(id)
1578
return os.path.basename(path)
1580
def get_dir(self, id):
1581
path = self.get_path(id)
1586
return os.path.dirname(path)
1588
def get_parent(self, id):
1589
if self.get_path(id) is None:
1591
directory = self.get_dir(id)
1592
if directory == '.':
1594
if directory is None:
1596
return self.get_rinventory().get(directory)