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 bzrlib.trace import mutter
22
# XXX: mbp: I'm not totally convinced that we should handle conflicts
23
# as part of changeset application, rather than only in the merge
26
"""Represent and apply a changeset
28
Conflicts in applying a changeset are represented as exceptions.
31
__docformat__ = "restructuredtext"
35
class OldFailedTreeOp(Exception):
37
Exception.__init__(self, "bzr-tree-change contains files from a"
38
" previous failed merge operation.")
39
def invert_dict(dict):
41
for (key,value) in dict.iteritems():
46
class PatchApply(object):
47
"""Patch application as a kind of content change"""
48
def __init__(self, contents):
51
:param contents: The text of the patch to apply
52
:type contents: str"""
53
self.contents = contents
55
def __eq__(self, other):
56
if not isinstance(other, PatchApply):
58
elif self.contents != other.contents:
63
def __ne__(self, other):
64
return not (self == other)
66
def apply(self, filename, conflict_handler, reverse=False):
67
"""Applies the patch to the specified file.
69
:param filename: the file to apply the patch to
71
:param reverse: If true, apply the patch in reverse
74
input_name = filename+".orig"
76
os.rename(filename, input_name)
78
if e.errno != errno.ENOENT:
80
if conflict_handler.patch_target_missing(filename, self.contents)\
83
os.rename(filename, input_name)
86
status = patch.patch(self.contents, input_name, filename,
88
os.chmod(filename, os.stat(input_name).st_mode)
92
conflict_handler.failed_hunks(filename)
95
class ChangeUnixPermissions(object):
96
"""This is two-way change, suitable for file modification, creation,
98
def __init__(self, old_mode, new_mode):
99
self.old_mode = old_mode
100
self.new_mode = new_mode
102
def apply(self, filename, conflict_handler, reverse=False):
104
from_mode = self.old_mode
105
to_mode = self.new_mode
107
from_mode = self.new_mode
108
to_mode = self.old_mode
110
current_mode = os.stat(filename).st_mode &0777
112
if e.errno == errno.ENOENT:
113
if conflict_handler.missing_for_chmod(filename) == "skip":
116
current_mode = from_mode
118
if from_mode is not None and current_mode != from_mode:
119
if conflict_handler.wrong_old_perms(filename, from_mode,
120
current_mode) != "continue":
123
if to_mode is not None:
125
os.chmod(filename, to_mode)
127
if e.errno == errno.ENOENT:
128
conflict_handler.missing_for_chmod(filename)
130
def __eq__(self, other):
131
if not isinstance(other, ChangeUnixPermissions):
133
elif self.old_mode != other.old_mode:
135
elif self.new_mode != other.new_mode:
140
def __ne__(self, other):
141
return not (self == other)
143
def dir_create(filename, conflict_handler, reverse):
144
"""Creates the directory, or deletes it if reverse is true. Intended to be
145
used with ReplaceContents.
147
:param filename: The name of the directory to create
149
:param reverse: If true, delete the directory, instead
156
if e.errno != errno.EEXIST:
158
if conflict_handler.dir_exists(filename) == "continue":
161
if e.errno == errno.ENOENT:
162
if conflict_handler.missing_parent(filename)=="continue":
163
file(filename, "wb").write(self.contents)
170
if conflict_handler.rmdir_non_empty(filename) == "skip":
177
class SymlinkCreate(object):
178
"""Creates or deletes a symlink (for use with ReplaceContents)"""
179
def __init__(self, contents):
182
:param contents: The filename of the target the symlink should point to
185
self.target = contents
187
def __call__(self, filename, conflict_handler, reverse):
188
"""Creates or destroys the symlink.
190
:param filename: The name of the symlink to create
194
assert(os.readlink(filename) == self.target)
198
os.symlink(self.target, filename)
200
if e.errno != errno.EEXIST:
202
if conflict_handler.link_name_exists(filename) == "continue":
203
os.symlink(self.target, filename)
205
def __eq__(self, other):
206
if not isinstance(other, SymlinkCreate):
208
elif self.target != other.target:
213
def __ne__(self, other):
214
return not (self == other)
216
class FileCreate(object):
217
"""Create or delete a file (for use with ReplaceContents)"""
218
def __init__(self, contents):
221
:param contents: The contents of the file to write
224
self.contents = contents
227
return "FileCreate(%i b)" % len(self.contents)
229
def __eq__(self, other):
230
if not isinstance(other, FileCreate):
232
elif self.contents != other.contents:
237
def __ne__(self, other):
238
return not (self == other)
240
def __call__(self, filename, conflict_handler, reverse):
241
"""Create or delete a file
243
:param filename: The name of the file to create
245
:param reverse: Delete the file instead of creating it
250
file(filename, "wb").write(self.contents)
252
if e.errno == errno.ENOENT:
253
if conflict_handler.missing_parent(filename)=="continue":
254
file(filename, "wb").write(self.contents)
260
if (file(filename, "rb").read() != self.contents):
261
direction = conflict_handler.wrong_old_contents(filename,
263
if direction != "continue":
267
if e.errno != errno.ENOENT:
269
if conflict_handler.missing_for_rm(filename, undo) == "skip":
274
def reversed(sequence):
275
max = len(sequence) - 1
276
for i in range(len(sequence)):
277
yield sequence[max - i]
279
class ReplaceContents(object):
280
"""A contents-replacement framework. It allows a file/directory/symlink to
281
be created, deleted, or replaced with another file/directory/symlink.
282
Arguments must be callable with (filename, reverse).
284
def __init__(self, old_contents, new_contents):
287
:param old_contents: The change to reverse apply (e.g. a deletion), \
289
:type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
291
:param new_contents: The second change to apply (e.g. a creation), \
293
:type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
296
self.old_contents=old_contents
297
self.new_contents=new_contents
300
return "ReplaceContents(%r -> %r)" % (self.old_contents,
303
def __eq__(self, other):
304
if not isinstance(other, ReplaceContents):
306
elif self.old_contents != other.old_contents:
308
elif self.new_contents != other.new_contents:
312
def __ne__(self, other):
313
return not (self == other)
315
def apply(self, filename, conflict_handler, reverse=False):
316
"""Applies the FileReplacement to the specified filename
318
:param filename: The name of the file to apply changes to
320
:param reverse: If true, apply the change in reverse
324
undo = self.old_contents
325
perform = self.new_contents
327
undo = self.new_contents
328
perform = self.old_contents
332
mode = os.lstat(filename).st_mode
333
if stat.S_ISLNK(mode):
336
if e.errno != errno.ENOENT:
338
if conflict_handler.missing_for_rm(filename, undo) == "skip":
340
undo(filename, conflict_handler, reverse=True)
341
if perform is not None:
342
perform(filename, conflict_handler, reverse=False)
344
os.chmod(filename, mode)
346
class ApplySequence(object):
347
def __init__(self, changes=None):
349
if changes is not None:
350
self.changes.extend(changes)
352
def __eq__(self, other):
353
if not isinstance(other, ApplySequence):
355
elif len(other.changes) != len(self.changes):
358
for i in range(len(self.changes)):
359
if self.changes[i] != other.changes[i]:
363
def __ne__(self, other):
364
return not (self == other)
367
def apply(self, filename, conflict_handler, reverse=False):
371
iter = reversed(self.changes)
373
change.apply(filename, conflict_handler, reverse)
376
class Diff3Merge(object):
377
def __init__(self, base_file, other_file):
378
self.base_file = base_file
379
self.other_file = other_file
381
def __eq__(self, other):
382
if not isinstance(other, Diff3Merge):
384
return (self.base_file == other.base_file and
385
self.other_file == other.other_file)
387
def __ne__(self, other):
388
return not (self == other)
390
def apply(self, filename, conflict_handler, reverse=False):
391
new_file = filename+".new"
393
base = self.base_file
394
other = self.other_file
396
base = self.other_file
397
other = self.base_file
398
status = patch.diff3(new_file, filename, base, other)
400
os.chmod(new_file, os.stat(filename).st_mode)
401
os.rename(new_file, filename)
405
conflict_handler.merge_conflict(new_file, filename, base, other)
409
"""Convenience function to create a directory.
411
:return: A ReplaceContents that will create a directory
412
:rtype: `ReplaceContents`
414
return ReplaceContents(None, dir_create)
417
"""Convenience function to delete a directory.
419
:return: A ReplaceContents that will delete a directory
420
:rtype: `ReplaceContents`
422
return ReplaceContents(dir_create, None)
424
def CreateFile(contents):
425
"""Convenience fucntion to create a file.
427
:param contents: The contents of the file to create
429
:return: A ReplaceContents that will create a file
430
:rtype: `ReplaceContents`
432
return ReplaceContents(None, FileCreate(contents))
434
def DeleteFile(contents):
435
"""Convenience fucntion to delete a file.
437
:param contents: The contents of the file to delete
439
:return: A ReplaceContents that will delete a file
440
:rtype: `ReplaceContents`
442
return ReplaceContents(FileCreate(contents), None)
444
def ReplaceFileContents(old_contents, new_contents):
445
"""Convenience fucntion to replace the contents of a file.
447
:param old_contents: The contents of the file to replace
448
:type old_contents: str
449
:param new_contents: The contents to replace the file with
450
:type new_contents: str
451
:return: A ReplaceContents that will replace the contents of a file a file
452
:rtype: `ReplaceContents`
454
return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
456
def CreateSymlink(target):
457
"""Convenience fucntion to create a symlink.
459
:param target: The path the link should point to
461
:return: A ReplaceContents that will delete a file
462
:rtype: `ReplaceContents`
464
return ReplaceContents(None, SymlinkCreate(target))
466
def DeleteSymlink(target):
467
"""Convenience fucntion to delete a symlink.
469
:param target: The path the link should point to
471
:return: A ReplaceContents that will delete a file
472
:rtype: `ReplaceContents`
474
return ReplaceContents(SymlinkCreate(target), None)
476
def ChangeTarget(old_target, new_target):
477
"""Convenience fucntion to change the target of a symlink.
479
:param old_target: The current link target
480
:type old_target: str
481
:param new_target: The new link target to use
482
:type new_target: str
483
:return: A ReplaceContents that will delete a file
484
:rtype: `ReplaceContents`
486
return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
489
class InvalidEntry(Exception):
490
"""Raise when a ChangesetEntry is invalid in some way"""
491
def __init__(self, entry, problem):
494
:param entry: The invalid ChangesetEntry
495
:type entry: `ChangesetEntry`
496
:param problem: The problem with the entry
499
msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id,
502
Exception.__init__(self, msg)
506
class SourceRootHasName(InvalidEntry):
507
"""This changeset entry has a name other than "", but its parent is !NULL"""
508
def __init__(self, entry, name):
511
:param entry: The invalid ChangesetEntry
512
:type entry: `ChangesetEntry`
513
:param name: The name of the entry
516
msg = 'Child of !NULL is named "%s", not "./.".' % name
517
InvalidEntry.__init__(self, entry, msg)
519
class NullIDAssigned(InvalidEntry):
520
"""The id !NULL was assigned to a real entry"""
521
def __init__(self, entry):
524
:param entry: The invalid ChangesetEntry
525
:type entry: `ChangesetEntry`
527
msg = '"!NULL" id assigned to a file "%s".' % entry.path
528
InvalidEntry.__init__(self, entry, msg)
530
class ParentIDIsSelf(InvalidEntry):
531
"""An entry is marked as its own parent"""
532
def __init__(self, entry):
535
:param entry: The invalid ChangesetEntry
536
:type entry: `ChangesetEntry`
538
msg = 'file %s has "%s" id for both self id and parent id.' % \
539
(entry.path, entry.id)
540
InvalidEntry.__init__(self, entry, msg)
542
class ChangesetEntry(object):
543
"""An entry the changeset"""
544
def __init__(self, id, parent, path):
545
"""Constructor. Sets parent and name assuming it was not
546
renamed/created/deleted.
547
:param id: The id associated with the entry
548
:param parent: The id of the parent of this entry (or !NULL if no
550
:param path: The file path relative to the tree root of this entry
556
self.new_parent = parent
557
self.contents_change = None
558
self.metadata_change = None
559
if parent == NULL_ID and path !='./.':
560
raise SourceRootHasName(self, path)
561
if self.id == NULL_ID:
562
raise NullIDAssigned(self)
563
if self.id == self.parent:
564
raise ParentIDIsSelf(self)
567
return "ChangesetEntry(%s)" % self.id
570
if self.path is None:
572
return os.path.dirname(self.path)
574
def __set_dir(self, dir):
575
self.path = os.path.join(dir, os.path.basename(self.path))
577
dir = property(__get_dir, __set_dir)
579
def __get_name(self):
580
if self.path is None:
582
return os.path.basename(self.path)
584
def __set_name(self, name):
585
self.path = os.path.join(os.path.dirname(self.path), name)
587
name = property(__get_name, __set_name)
589
def __get_new_dir(self):
590
if self.new_path is None:
592
return os.path.dirname(self.new_path)
594
def __set_new_dir(self, dir):
595
self.new_path = os.path.join(dir, os.path.basename(self.new_path))
597
new_dir = property(__get_new_dir, __set_new_dir)
599
def __get_new_name(self):
600
if self.new_path is None:
602
return os.path.basename(self.new_path)
604
def __set_new_name(self, name):
605
self.new_path = os.path.join(os.path.dirname(self.new_path), name)
607
new_name = property(__get_new_name, __set_new_name)
609
def needs_rename(self):
610
"""Determines whether the entry requires renaming.
615
return (self.parent != self.new_parent or self.name != self.new_name)
617
def is_deletion(self, reverse):
618
"""Return true if applying the entry would delete a file/directory.
620
:param reverse: if true, the changeset is being applied in reverse
623
return ((self.new_parent is None and not reverse) or
624
(self.parent is None and reverse))
626
def is_creation(self, reverse):
627
"""Return true if applying the entry would create a file/directory.
629
:param reverse: if true, the changeset is being applied in reverse
632
return ((self.parent is None and not reverse) or
633
(self.new_parent is None and reverse))
635
def is_creation_or_deletion(self):
636
"""Return true if applying the entry would create or delete a
641
return self.parent is None or self.new_parent is None
643
def get_cset_path(self, mod=False):
644
"""Determine the path of the entry according to the changeset.
646
:param changeset: The changeset to derive the path from
647
:type changeset: `Changeset`
648
:param mod: If true, generate the MOD path. Otherwise, generate the \
650
:return: the path of the entry, or None if it did not exist in the \
652
:rtype: str or NoneType
655
if self.new_parent == NULL_ID:
657
elif self.new_parent is None:
661
if self.parent == NULL_ID:
663
elif self.parent is None:
667
def summarize_name(self, changeset, reverse=False):
668
"""Produce a one-line summary of the filename. Indicates renames as
669
old => new, indicates creation as None => new, indicates deletion as
672
:param changeset: The changeset to get paths from
673
:type changeset: `Changeset`
674
:param reverse: If true, reverse the names in the output
678
orig_path = self.get_cset_path(False)
679
mod_path = self.get_cset_path(True)
680
if orig_path is not None:
681
orig_path = orig_path[2:]
682
if mod_path is not None:
683
mod_path = mod_path[2:]
684
if orig_path == mod_path:
688
return "%s => %s" % (orig_path, mod_path)
690
return "%s => %s" % (mod_path, orig_path)
693
def get_new_path(self, id_map, changeset, reverse=False):
694
"""Determine the full pathname to rename to
696
:param id_map: The map of ids to filenames for the tree
697
:type id_map: Dictionary
698
:param changeset: The changeset to get data from
699
:type changeset: `Changeset`
700
:param reverse: If true, we're applying the changeset in reverse
704
mutter("Finding new path for %s" % self.summarize_name(changeset))
708
from_dir = self.new_dir
710
from_name = self.new_name
712
parent = self.new_parent
713
to_dir = self.new_dir
715
to_name = self.new_name
716
from_name = self.name
721
if parent == NULL_ID or parent is None:
723
raise SourceRootHasName(self, to_name)
726
if from_dir == to_dir:
727
dir = os.path.dirname(id_map[self.id])
729
mutter("path, new_path: %r %r" % (self.path, self.new_path))
730
parent_entry = changeset.entries[parent]
731
dir = parent_entry.get_new_path(id_map, changeset, reverse)
732
if from_name == to_name:
733
name = os.path.basename(id_map[self.id])
736
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
737
return os.path.join(dir, name)
740
"""Determines whether the entry does nothing
742
:return: True if the entry does no renames or content changes
745
if self.contents_change is not None:
747
elif self.metadata_change is not None:
749
elif self.parent != self.new_parent:
751
elif self.name != self.new_name:
756
def apply(self, filename, conflict_handler, reverse=False):
757
"""Applies the file content and/or metadata changes.
759
:param filename: the filename of the entry
761
:param reverse: If true, apply the changes in reverse
764
if self.is_deletion(reverse) and self.metadata_change is not None:
765
self.metadata_change.apply(filename, conflict_handler, reverse)
766
if self.contents_change is not None:
767
self.contents_change.apply(filename, conflict_handler, reverse)
768
if not self.is_deletion(reverse) and self.metadata_change is not None:
769
self.metadata_change.apply(filename, conflict_handler, reverse)
771
class IDPresent(Exception):
772
def __init__(self, id):
773
msg = "Cannot add entry because that id has already been used:\n%s" %\
775
Exception.__init__(self, msg)
778
class Changeset(object):
779
"""A set of changes to apply"""
783
def add_entry(self, entry):
784
"""Add an entry to the list of entries"""
785
if self.entries.has_key(entry.id):
786
raise IDPresent(entry.id)
787
self.entries[entry.id] = entry
789
def my_sort(sequence, key, reverse=False):
790
"""A sort function that supports supplying a key for comparison
792
:param sequence: The sequence to sort
793
:param key: A callable object that returns the values to be compared
794
:param reverse: If true, sort in reverse order
797
def cmp_by_key(entry_a, entry_b):
802
return cmp(key(entry_a), key(entry_b))
803
sequence.sort(cmp_by_key)
805
def get_rename_entries(changeset, inventory, reverse):
806
"""Return a list of entries that will be renamed. Entries are sorted from
807
longest to shortest source path and from shortest to longest target path.
809
:param changeset: The changeset to look in
810
:type changeset: `Changeset`
811
:param inventory: The source of current tree paths for the given ids
812
:type inventory: Dictionary
813
:param reverse: If true, the changeset is being applied in reverse
815
:return: source entries and target entries as a tuple
818
source_entries = [x for x in changeset.entries.itervalues()
820
# these are done from longest path to shortest, to avoid deleting a
821
# parent before its children are deleted/renamed
822
def longest_to_shortest(entry):
823
path = inventory.get(entry.id)
828
my_sort(source_entries, longest_to_shortest, reverse=True)
830
target_entries = source_entries[:]
831
# These are done from shortest to longest path, to avoid creating a
832
# child before its parent has been created/renamed
833
def shortest_to_longest(entry):
834
path = entry.get_new_path(inventory, changeset, reverse)
839
my_sort(target_entries, shortest_to_longest)
840
return (source_entries, target_entries)
842
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
843
conflict_handler, reverse):
844
"""Delete and rename entries as appropriate. Entries are renamed to temp
845
names. A map of id -> temp name (or None, for deletions) is returned.
847
:param source_entries: The entries to rename and delete
848
:type source_entries: List of `ChangesetEntry`
849
:param inventory: The map of id -> filename in the current tree
850
:type inventory: Dictionary
851
:param dir: The directory to apply changes to
853
:param reverse: Apply changes in reverse
855
:return: a mapping of id to temporary name
859
for i in range(len(source_entries)):
860
entry = source_entries[i]
861
if entry.is_deletion(reverse):
862
path = os.path.join(dir, inventory[entry.id])
863
entry.apply(path, conflict_handler, reverse)
864
temp_name[entry.id] = None
867
to_name = os.path.join(temp_dir, str(i))
868
src_path = inventory.get(entry.id)
869
if src_path is not None:
870
src_path = os.path.join(dir, src_path)
872
os.rename(src_path, to_name)
873
temp_name[entry.id] = to_name
875
if e.errno != errno.ENOENT:
877
if conflict_handler.missing_for_rename(src_path) == "skip":
883
def rename_to_new_create(changed_inventory, target_entries, inventory,
884
changeset, dir, conflict_handler, reverse):
885
"""Rename entries with temp names to their final names, create new files.
887
:param changed_inventory: A mapping of id to temporary name
888
:type changed_inventory: Dictionary
889
:param target_entries: The entries to apply changes to
890
:type target_entries: List of `ChangesetEntry`
891
:param changeset: The changeset to apply
892
:type changeset: `Changeset`
893
:param dir: The directory to apply changes to
895
:param reverse: If true, apply changes in reverse
898
for entry in target_entries:
899
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
900
if new_tree_path is None:
902
new_path = os.path.join(dir, new_tree_path)
903
old_path = changed_inventory.get(entry.id)
904
if os.path.exists(new_path):
905
if conflict_handler.target_exists(entry, new_path, old_path) == \
908
if entry.is_creation(reverse):
909
entry.apply(new_path, conflict_handler, reverse)
910
changed_inventory[entry.id] = new_tree_path
915
os.rename(old_path, new_path)
916
changed_inventory[entry.id] = new_tree_path
918
raise Exception ("%s is missing" % new_path)
920
class TargetExists(Exception):
921
def __init__(self, entry, target):
922
msg = "The path %s already exists" % target
923
Exception.__init__(self, msg)
927
class RenameConflict(Exception):
928
def __init__(self, id, this_name, base_name, other_name):
929
msg = """Trees all have different names for a file
933
id: %s""" % (this_name, base_name, other_name, id)
934
Exception.__init__(self, msg)
935
self.this_name = this_name
936
self.base_name = base_name
937
self_other_name = other_name
939
class MoveConflict(Exception):
940
def __init__(self, id, this_parent, base_parent, other_parent):
941
msg = """The file is in different directories in every tree
945
id: %s""" % (this_parent, base_parent, other_parent, id)
946
Exception.__init__(self, msg)
947
self.this_parent = this_parent
948
self.base_parent = base_parent
949
self_other_parent = other_parent
951
class MergeConflict(Exception):
952
def __init__(self, this_path):
953
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
954
self.this_path = this_path
956
class MergePermissionConflict(Exception):
957
def __init__(self, this_path, base_path, other_path):
958
this_perms = os.stat(this_path).st_mode & 0755
959
base_perms = os.stat(base_path).st_mode & 0755
960
other_perms = os.stat(other_path).st_mode & 0755
961
msg = """Conflicting permission for %s
965
""" % (this_path, this_perms, base_perms, other_perms)
966
self.this_path = this_path
967
self.base_path = base_path
968
self.other_path = other_path
969
Exception.__init__(self, msg)
971
class WrongOldContents(Exception):
972
def __init__(self, filename):
973
msg = "Contents mismatch deleting %s" % filename
974
self.filename = filename
975
Exception.__init__(self, msg)
977
class WrongOldPermissions(Exception):
978
def __init__(self, filename, old_perms, new_perms):
979
msg = "Permission missmatch on %s:\n" \
980
"Expected 0%o, got 0%o." % (filename, old_perms, new_perms)
981
self.filename = filename
982
Exception.__init__(self, msg)
984
class RemoveContentsConflict(Exception):
985
def __init__(self, filename):
986
msg = "Conflict deleting %s, which has different contents in BASE"\
987
" and THIS" % filename
988
self.filename = filename
989
Exception.__init__(self, msg)
991
class DeletingNonEmptyDirectory(Exception):
992
def __init__(self, filename):
993
msg = "Trying to remove dir %s while it still had files" % filename
994
self.filename = filename
995
Exception.__init__(self, msg)
998
class PatchTargetMissing(Exception):
999
def __init__(self, filename):
1000
msg = "Attempt to patch %s, which does not exist" % filename
1001
Exception.__init__(self, msg)
1002
self.filename = filename
1004
class MissingPermsFile(Exception):
1005
def __init__(self, filename):
1006
msg = "Attempt to change permissions on %s, which does not exist" %\
1008
Exception.__init__(self, msg)
1009
self.filename = filename
1011
class MissingForRm(Exception):
1012
def __init__(self, filename):
1013
msg = "Attempt to remove missing path %s" % filename
1014
Exception.__init__(self, msg)
1015
self.filename = filename
1018
class MissingForRename(Exception):
1019
def __init__(self, filename):
1020
msg = "Attempt to move missing path %s" % (filename)
1021
Exception.__init__(self, msg)
1022
self.filename = filename
1024
class NewContentsConflict(Exception):
1025
def __init__(self, filename):
1026
msg = "Conflicting contents for new file %s" % (filename)
1027
Exception.__init__(self, msg)
1030
class MissingForMerge(Exception):
1031
def __init__(self, filename):
1032
msg = "The file %s was modified, but does not exist in this tree"\
1034
Exception.__init__(self, msg)
1037
class ExceptionConflictHandler(object):
1038
"""Default handler for merge exceptions.
1040
This throws an error on any kind of conflict. Conflict handlers can
1041
descend from this class if they have a better way to handle some or
1042
all types of conflict.
1044
def __init__(self, dir):
1047
def missing_parent(self, pathname):
1048
parent = os.path.dirname(pathname)
1049
raise Exception("Parent directory missing for %s" % pathname)
1051
def dir_exists(self, pathname):
1052
raise Exception("Directory already exists for %s" % pathname)
1054
def failed_hunks(self, pathname):
1055
raise Exception("Failed to apply some hunks for %s" % pathname)
1057
def target_exists(self, entry, target, old_path):
1058
raise TargetExists(entry, target)
1060
def rename_conflict(self, id, this_name, base_name, other_name):
1061
raise RenameConflict(id, this_name, base_name, other_name)
1063
def move_conflict(self, id, inventory):
1064
this_dir = inventory.this.get_dir(id)
1065
base_dir = inventory.base.get_dir(id)
1066
other_dir = inventory.other.get_dir(id)
1067
raise MoveConflict(id, this_dir, base_dir, other_dir)
1069
def merge_conflict(self, new_file, this_path, base_path, other_path):
1071
raise MergeConflict(this_path)
1073
def permission_conflict(self, this_path, base_path, other_path):
1074
raise MergePermissionConflict(this_path, base_path, other_path)
1076
def wrong_old_contents(self, filename, expected_contents):
1077
raise WrongOldContents(filename)
1079
def rem_contents_conflict(self, filename, this_contents, base_contents):
1080
raise RemoveContentsConflict(filename)
1082
def wrong_old_perms(self, filename, old_perms, new_perms):
1083
raise WrongOldPermissions(filename, old_perms, new_perms)
1085
def rmdir_non_empty(self, filename):
1086
raise DeletingNonEmptyDirectory(filename)
1088
def link_name_exists(self, filename):
1089
raise TargetExists(filename)
1091
def patch_target_missing(self, filename, contents):
1092
raise PatchTargetMissing(filename)
1094
def missing_for_chmod(self, filename):
1095
raise MissingPermsFile(filename)
1097
def missing_for_rm(self, filename, change):
1098
raise MissingForRm(filename)
1100
def missing_for_rename(self, filename):
1101
raise MissingForRename(filename)
1103
def missing_for_merge(self, file_id, inventory):
1104
raise MissingForMerge(inventory.other.get_path(file_id))
1106
def new_contents_conflict(self, filename, other_contents):
1107
raise NewContentsConflict(filename)
1112
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1114
"""Apply a changeset to a directory.
1116
:param changeset: The changes to perform
1117
:type changeset: `Changeset`
1118
:param inventory: The mapping of id to filename for the directory
1119
:type inventory: Dictionary
1120
:param dir: The path of the directory to apply the changes to
1122
:param reverse: If true, apply the changes in reverse
1124
:return: The mapping of the changed entries
1127
if conflict_handler is None:
1128
conflict_handler = ExceptionConflictHandler(dir)
1129
temp_dir = os.path.join(dir, "bzr-tree-change")
1133
if e.errno == errno.EEXIST:
1137
if e.errno == errno.ENOTEMPTY:
1138
raise OldFailedTreeOp()
1143
#apply changes that don't affect filenames
1144
for entry in changeset.entries.itervalues():
1145
if not entry.is_creation_or_deletion():
1146
path = os.path.join(dir, inventory[entry.id])
1147
entry.apply(path, conflict_handler, reverse)
1149
# Apply renames in stages, to minimize conflicts:
1150
# Only files whose name or parent change are interesting, because their
1151
# target name may exist in the source tree. If a directory's name changes,
1152
# that doesn't make its children interesting.
1153
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1156
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1157
temp_dir, conflict_handler,
1160
rename_to_new_create(changed_inventory, target_entries, inventory,
1161
changeset, dir, conflict_handler, reverse)
1163
return changed_inventory
1166
def apply_changeset_tree(cset, tree, reverse=False):
1168
for entry in tree.source_inventory().itervalues():
1169
inventory[entry.id] = entry.path
1170
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1172
new_entries, remove_entries = \
1173
get_inventory_change(inventory, new_inventory, cset, reverse)
1174
tree.update_source_inventory(new_entries, remove_entries)
1177
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1180
for entry in cset.entries.itervalues():
1181
if entry.needs_rename():
1182
new_path = entry.get_new_path(inventory, cset)
1183
if new_path is None:
1184
remove_entries.append(entry.id)
1186
new_entries[new_path] = entry.id
1187
return new_entries, remove_entries
1190
def print_changeset(cset):
1191
"""Print all non-boring changeset entries
1193
:param cset: The changeset to print
1194
:type cset: `Changeset`
1196
for entry in cset.entries.itervalues():
1197
if entry.is_boring():
1200
print entry.summarize_name(cset)
1202
class CompositionFailure(Exception):
1203
def __init__(self, old_entry, new_entry, problem):
1204
msg = "Unable to conpose entries.\n %s" % problem
1205
Exception.__init__(self, msg)
1207
class IDMismatch(CompositionFailure):
1208
def __init__(self, old_entry, new_entry):
1209
problem = "Attempt to compose entries with different ids: %s and %s" %\
1210
(old_entry.id, new_entry.id)
1211
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1213
def compose_changesets(old_cset, new_cset):
1214
"""Combine two changesets into one. This works well for exact patching.
1215
Otherwise, not so well.
1217
:param old_cset: The first changeset that would be applied
1218
:type old_cset: `Changeset`
1219
:param new_cset: The second changeset that would be applied
1220
:type new_cset: `Changeset`
1221
:return: A changeset that combines the changes in both changesets
1224
composed = Changeset()
1225
for old_entry in old_cset.entries.itervalues():
1226
new_entry = new_cset.entries.get(old_entry.id)
1227
if new_entry is None:
1228
composed.add_entry(old_entry)
1230
composed_entry = compose_entries(old_entry, new_entry)
1231
if composed_entry.parent is not None or\
1232
composed_entry.new_parent is not None:
1233
composed.add_entry(composed_entry)
1234
for new_entry in new_cset.entries.itervalues():
1235
if not old_cset.entries.has_key(new_entry.id):
1236
composed.add_entry(new_entry)
1239
def compose_entries(old_entry, new_entry):
1240
"""Combine two entries into one.
1242
:param old_entry: The first entry that would be applied
1243
:type old_entry: ChangesetEntry
1244
:param old_entry: The second entry that would be applied
1245
:type old_entry: ChangesetEntry
1246
:return: A changeset entry combining both entries
1247
:rtype: `ChangesetEntry`
1249
if old_entry.id != new_entry.id:
1250
raise IDMismatch(old_entry, new_entry)
1251
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1253
if (old_entry.parent != old_entry.new_parent or
1254
new_entry.parent != new_entry.new_parent):
1255
output.new_parent = new_entry.new_parent
1257
if (old_entry.path != old_entry.new_path or
1258
new_entry.path != new_entry.new_path):
1259
output.new_path = new_entry.new_path
1261
output.contents_change = compose_contents(old_entry, new_entry)
1262
output.metadata_change = compose_metadata(old_entry, new_entry)
1265
def compose_contents(old_entry, new_entry):
1266
"""Combine the contents of two changeset entries. Entries are combined
1267
intelligently where possible, but the fallback behavior returns an
1270
:param old_entry: The first entry that would be applied
1271
:type old_entry: `ChangesetEntry`
1272
:param new_entry: The second entry that would be applied
1273
:type new_entry: `ChangesetEntry`
1274
:return: A combined contents change
1275
:rtype: anything supporting the apply(reverse=False) method
1277
old_contents = old_entry.contents_change
1278
new_contents = new_entry.contents_change
1279
if old_entry.contents_change is None:
1280
return new_entry.contents_change
1281
elif new_entry.contents_change is None:
1282
return old_entry.contents_change
1283
elif isinstance(old_contents, ReplaceContents) and \
1284
isinstance(new_contents, ReplaceContents):
1285
if old_contents.old_contents == new_contents.new_contents:
1288
return ReplaceContents(old_contents.old_contents,
1289
new_contents.new_contents)
1290
elif isinstance(old_contents, ApplySequence):
1291
output = ApplySequence(old_contents.changes)
1292
if isinstance(new_contents, ApplySequence):
1293
output.changes.extend(new_contents.changes)
1295
output.changes.append(new_contents)
1297
elif isinstance(new_contents, ApplySequence):
1298
output = ApplySequence((old_contents.changes,))
1299
output.extend(new_contents.changes)
1302
return ApplySequence((old_contents, new_contents))
1304
def compose_metadata(old_entry, new_entry):
1305
old_meta = old_entry.metadata_change
1306
new_meta = new_entry.metadata_change
1307
if old_meta is None:
1309
elif new_meta is None:
1311
elif isinstance(old_meta, ChangeUnixPermissions) and \
1312
isinstance(new_meta, ChangeUnixPermissions):
1313
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1315
return ApplySequence(old_meta, new_meta)
1318
def changeset_is_null(changeset):
1319
for entry in changeset.entries.itervalues():
1320
if not entry.is_boring():
1324
class UnsuppportedFiletype(Exception):
1325
def __init__(self, full_path, stat_result):
1326
msg = "The file \"%s\" is not a supported filetype." % full_path
1327
Exception.__init__(self, msg)
1328
self.full_path = full_path
1329
self.stat_result = stat_result
1331
def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None):
1332
return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)()
1334
class ChangesetGenerator(object):
1335
def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None):
1336
object.__init__(self)
1337
self.tree_a = tree_a
1338
self.tree_b = tree_b
1339
if inventory_a is not None:
1340
self.inventory_a = inventory_a
1342
self.inventory_a = tree_a.inventory()
1343
if inventory_b is not None:
1344
self.inventory_b = inventory_b
1346
self.inventory_b = tree_b.inventory()
1347
self.r_inventory_a = self.reverse_inventory(self.inventory_a)
1348
self.r_inventory_b = self.reverse_inventory(self.inventory_b)
1350
def reverse_inventory(self, inventory):
1352
for entry in inventory.itervalues():
1353
if entry.id is None:
1355
r_inventory[entry.id] = entry
1360
for entry in self.inventory_a.itervalues():
1361
if entry.id is None:
1363
cs_entry = self.make_entry(entry.id)
1364
if cs_entry is not None and not cs_entry.is_boring():
1365
cset.add_entry(cs_entry)
1367
for entry in self.inventory_b.itervalues():
1368
if entry.id is None:
1370
if not self.r_inventory_a.has_key(entry.id):
1371
cs_entry = self.make_entry(entry.id)
1372
if cs_entry is not None and not cs_entry.is_boring():
1373
cset.add_entry(cs_entry)
1374
for entry in list(cset.entries.itervalues()):
1375
if entry.parent != entry.new_parent:
1376
if not cset.entries.has_key(entry.parent) and\
1377
entry.parent != NULL_ID and entry.parent is not None:
1378
parent_entry = self.make_boring_entry(entry.parent)
1379
cset.add_entry(parent_entry)
1380
if not cset.entries.has_key(entry.new_parent) and\
1381
entry.new_parent != NULL_ID and \
1382
entry.new_parent is not None:
1383
parent_entry = self.make_boring_entry(entry.new_parent)
1384
cset.add_entry(parent_entry)
1387
def get_entry_parent(self, entry, inventory):
1390
if entry.path == "./.":
1392
dirname = os.path.dirname(entry.path)
1395
parent = inventory[dirname]
1398
def get_path(self, entry, tree):
1401
if entry.path == ".":
1405
def make_basic_entry(self, id, only_interesting):
1406
entry_a = self.r_inventory_a.get(id)
1407
entry_b = self.r_inventory_b.get(id)
1408
if only_interesting and not self.is_interesting(entry_a, entry_b):
1410
parent = self.get_entry_parent(entry_a, self.inventory_a)
1411
path = self.get_path(entry_a, self.tree_a)
1412
cs_entry = ChangesetEntry(id, parent, path)
1413
new_parent = self.get_entry_parent(entry_b, self.inventory_b)
1416
new_path = self.get_path(entry_b, self.tree_b)
1418
cs_entry.new_path = new_path
1419
cs_entry.new_parent = new_parent
1422
def is_interesting(self, entry_a, entry_b):
1423
if entry_a is not None:
1424
if entry_a.interesting:
1426
if entry_b is not None:
1427
if entry_b.interesting:
1431
def make_boring_entry(self, id):
1432
cs_entry = self.make_basic_entry(id, only_interesting=False)
1433
if cs_entry.is_creation_or_deletion():
1434
return self.make_entry(id, only_interesting=False)
1439
def make_entry(self, id, only_interesting=True):
1440
cs_entry = self.make_basic_entry(id, only_interesting)
1442
if cs_entry is None:
1444
if id in self.tree_a and id in self.tree_b:
1445
a_sha1 = self.tree_a.get_file_sha1(id)
1446
b_sha1 = self.tree_b.get_file_sha1(id)
1447
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1450
full_path_a = self.tree_a.readonly_path(id)
1451
full_path_b = self.tree_b.readonly_path(id)
1452
stat_a = self.lstat(full_path_a)
1453
stat_b = self.lstat(full_path_b)
1455
cs_entry.new_parent = None
1456
cs_entry.new_path = None
1458
cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1459
cs_entry.contents_change = self.make_contents_change(full_path_a,
1465
def make_mode_change(self, stat_a, stat_b):
1467
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1468
mode_a = stat_a.st_mode & 0777
1470
if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1471
mode_b = stat_b.st_mode & 0777
1472
if mode_a == mode_b:
1474
return ChangeUnixPermissions(mode_a, mode_b)
1476
def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1477
if stat_a is None and stat_b is None:
1479
if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1480
stat.S_ISDIR(stat_b.st_mode):
1482
if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1483
stat.S_ISREG(stat_b.st_mode):
1484
if stat_a.st_ino == stat_b.st_ino and \
1485
stat_a.st_dev == stat_b.st_dev:
1487
if file(full_path_a, "rb").read() == \
1488
file(full_path_b, "rb").read():
1491
patch_contents = patch.diff(full_path_a,
1492
file(full_path_b, "rb").read())
1493
if patch_contents is None:
1495
return PatchApply(patch_contents)
1497
a_contents = self.get_contents(stat_a, full_path_a)
1498
b_contents = self.get_contents(stat_b, full_path_b)
1499
if a_contents == b_contents:
1501
return ReplaceContents(a_contents, b_contents)
1503
def get_contents(self, stat_result, full_path):
1504
if stat_result is None:
1506
elif stat.S_ISREG(stat_result.st_mode):
1507
return FileCreate(file(full_path, "rb").read())
1508
elif stat.S_ISDIR(stat_result.st_mode):
1510
elif stat.S_ISLNK(stat_result.st_mode):
1511
return SymlinkCreate(os.readlink(full_path))
1513
raise UnsupportedFiletype(full_path, stat_result)
1515
def lstat(self, full_path):
1517
if full_path is not None:
1519
stat_result = os.lstat(full_path)
1521
if e.errno != errno.ENOENT:
1526
def full_path(entry, tree):
1527
return os.path.join(tree.root, entry.path)
1529
def new_delete_entry(entry, tree, inventory, delete):
1530
if entry.path == "":
1533
parent = inventory[dirname(entry.path)].id
1534
cs_entry = ChangesetEntry(parent, entry.path)
1536
cs_entry.new_path = None
1537
cs_entry.new_parent = None
1539
cs_entry.path = None
1540
cs_entry.parent = None
1541
full_path = full_path(entry, tree)
1542
status = os.lstat(full_path)
1543
if stat.S_ISDIR(file_stat.st_mode):
1549
# XXX: Can't we unify this with the regular inventory object
1550
class Inventory(object):
1551
def __init__(self, inventory):
1552
self.inventory = inventory
1553
self.rinventory = None
1555
def get_rinventory(self):
1556
if self.rinventory is None:
1557
self.rinventory = invert_dict(self.inventory)
1558
return self.rinventory
1560
def get_path(self, id):
1561
return self.inventory.get(id)
1563
def get_name(self, id):
1564
path = self.get_path(id)
1568
return os.path.basename(path)
1570
def get_dir(self, id):
1571
path = self.get_path(id)
1576
return os.path.dirname(path)
1578
def get_parent(self, id):
1579
if self.get_path(id) is None:
1581
directory = self.get_dir(id)
1582
if directory == '.':
1584
if directory is None:
1586
return self.get_rinventory().get(directory)