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
35
from bzrlib.errors import BzrCheckError
37
__docformat__ = "restructuredtext"
41
class OldFailedTreeOp(Exception):
43
Exception.__init__(self, "bzr-tree-change contains files from a"
44
" previous failed merge operation.")
45
def invert_dict(dict):
47
for (key,value) in dict.iteritems():
52
class ChangeExecFlag(object):
53
"""This is two-way change, suitable for file modification, creation,
55
def __init__(self, old_exec_flag, new_exec_flag):
56
self.old_exec_flag = old_exec_flag
57
self.new_exec_flag = new_exec_flag
59
def apply(self, filename, conflict_handler, reverse=False):
61
from_exec_flag = self.old_exec_flag
62
to_exec_flag = self.new_exec_flag
64
from_exec_flag = self.new_exec_flag
65
to_exec_flag = self.old_exec_flag
67
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
69
if e.errno == errno.ENOENT:
70
if conflict_handler.missing_for_exec_flag(filename) == "skip":
73
current_exec_flag = from_exec_flag
75
if from_exec_flag is not None and current_exec_flag != from_exec_flag:
76
if conflict_handler.wrong_old_exec_flag(filename,
77
from_exec_flag, current_exec_flag) != "continue":
80
if to_exec_flag is not None:
81
current_mode = os.stat(filename).st_mode
85
to_mode = current_mode | (0100 & ~umask)
86
# Enable x-bit for others only if they can read it.
87
if current_mode & 0004:
88
to_mode |= 0001 & ~umask
89
if current_mode & 0040:
90
to_mode |= 0010 & ~umask
92
to_mode = current_mode & ~0111
94
os.chmod(filename, to_mode)
96
if e.errno == errno.ENOENT:
97
conflict_handler.missing_for_exec_flag(filename)
99
def __eq__(self, other):
100
return (isinstance(other, ChangeExecFlag) and
101
self.old_exec_flag == other.old_exec_flag and
102
self.new_exec_flag == other.new_exec_flag)
104
def __ne__(self, other):
105
return not (self == other)
108
def dir_create(filename, conflict_handler, reverse):
109
"""Creates the directory, or deletes it if reverse is true. Intended to be
110
used with ReplaceContents.
112
:param filename: The name of the directory to create
114
:param reverse: If true, delete the directory, instead
121
if e.errno != errno.EEXIST:
123
if conflict_handler.dir_exists(filename) == "continue":
126
if e.errno == errno.ENOENT:
127
if conflict_handler.missing_parent(filename)=="continue":
128
file(filename, "wb").write(self.contents)
133
if e.errno != errno.ENOTEMPTY:
135
if conflict_handler.rmdir_non_empty(filename) == "skip":
140
class SymlinkCreate(object):
141
"""Creates or deletes a symlink (for use with ReplaceContents)"""
142
def __init__(self, contents):
145
:param contents: The filename of the target the symlink should point to
148
self.target = contents
151
return "SymlinkCreate(%s)" % self.target
153
def __call__(self, filename, conflict_handler, reverse):
154
"""Creates or destroys the symlink.
156
:param filename: The name of the symlink to create
160
assert(os.readlink(filename) == self.target)
164
os.symlink(self.target, filename)
166
if e.errno != errno.EEXIST:
168
if conflict_handler.link_name_exists(filename) == "continue":
169
os.symlink(self.target, filename)
171
def __eq__(self, other):
172
if not isinstance(other, SymlinkCreate):
174
elif self.target != other.target:
179
def __ne__(self, other):
180
return not (self == other)
182
class FileCreate(object):
183
"""Create or delete a file (for use with ReplaceContents)"""
184
def __init__(self, contents):
187
:param contents: The contents of the file to write
190
self.contents = contents
193
return "FileCreate(%i b)" % len(self.contents)
195
def __eq__(self, other):
196
if not isinstance(other, FileCreate):
198
elif self.contents != other.contents:
203
def __ne__(self, other):
204
return not (self == other)
206
def __call__(self, filename, conflict_handler, reverse):
207
"""Create or delete a file
209
:param filename: The name of the file to create
211
:param reverse: Delete the file instead of creating it
216
file(filename, "wb").write(self.contents)
218
if e.errno == errno.ENOENT:
219
if conflict_handler.missing_parent(filename)=="continue":
220
file(filename, "wb").write(self.contents)
226
if (file(filename, "rb").read() != self.contents):
227
direction = conflict_handler.wrong_old_contents(filename,
229
if direction != "continue":
233
if e.errno != errno.ENOENT:
235
if conflict_handler.missing_for_rm(filename, undo) == "skip":
240
class TreeFileCreate(object):
241
"""Create or delete a file (for use with ReplaceContents)"""
242
def __init__(self, tree, file_id):
245
:param contents: The contents of the file to write
249
self.file_id = file_id
252
return "TreeFileCreate(%s)" % self.file_id
254
def __eq__(self, other):
255
if not isinstance(other, TreeFileCreate):
257
return self.tree.get_file_sha1(self.file_id) == \
258
other.tree.get_file_sha1(other.file_id)
260
def __ne__(self, other):
261
return not (self == other)
263
def write_file(self, filename):
264
outfile = file(filename, "wb")
265
for line in self.tree.get_file(self.file_id):
268
def same_text(self, filename):
269
in_file = file(filename, "rb")
270
return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
272
def __call__(self, filename, conflict_handler, reverse):
273
"""Create or delete a file
275
:param filename: The name of the file to create
277
:param reverse: Delete the file instead of creating it
282
self.write_file(filename)
284
if e.errno == errno.ENOENT:
285
if conflict_handler.missing_parent(filename)=="continue":
286
self.write_file(filename)
292
if not self.same_text(filename):
293
direction = conflict_handler.wrong_old_contents(filename,
294
self.tree.get_file(self.file_id).read())
295
if direction != "continue":
299
if e.errno != errno.ENOENT:
301
if conflict_handler.missing_for_rm(filename, undo) == "skip":
306
def reversed(sequence):
307
max = len(sequence) - 1
308
for i in range(len(sequence)):
309
yield sequence[max - i]
311
class ReplaceContents(object):
312
"""A contents-replacement framework. It allows a file/directory/symlink to
313
be created, deleted, or replaced with another file/directory/symlink.
314
Arguments must be callable with (filename, reverse).
316
def __init__(self, old_contents, new_contents):
319
:param old_contents: The change to reverse apply (e.g. a deletion), \
321
:type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
323
:param new_contents: The second change to apply (e.g. a creation), \
325
:type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
328
self.old_contents=old_contents
329
self.new_contents=new_contents
332
return "ReplaceContents(%r -> %r)" % (self.old_contents,
335
def __eq__(self, other):
336
if not isinstance(other, ReplaceContents):
338
elif self.old_contents != other.old_contents:
340
elif self.new_contents != other.new_contents:
344
def __ne__(self, other):
345
return not (self == other)
347
def apply(self, filename, conflict_handler, reverse=False):
348
"""Applies the FileReplacement to the specified filename
350
:param filename: The name of the file to apply changes to
352
:param reverse: If true, apply the change in reverse
356
undo = self.old_contents
357
perform = self.new_contents
359
undo = self.new_contents
360
perform = self.old_contents
364
mode = os.lstat(filename).st_mode
365
if stat.S_ISLNK(mode):
368
if e.errno != errno.ENOENT:
370
if conflict_handler.missing_for_rm(filename, undo) == "skip":
372
undo(filename, conflict_handler, reverse=True)
373
if perform is not None:
374
perform(filename, conflict_handler, reverse=False)
376
os.chmod(filename, mode)
378
def is_creation(self):
379
return self.new_contents is not None and self.old_contents is None
381
def is_deletion(self):
382
return self.old_contents is not None and self.new_contents is None
384
class ApplySequence(object):
385
def __init__(self, changes=None):
387
if changes is not None:
388
self.changes.extend(changes)
390
def __eq__(self, other):
391
if not isinstance(other, ApplySequence):
393
elif len(other.changes) != len(self.changes):
396
for i in range(len(self.changes)):
397
if self.changes[i] != other.changes[i]:
401
def __ne__(self, other):
402
return not (self == other)
405
def apply(self, filename, conflict_handler, reverse=False):
409
iter = reversed(self.changes)
411
change.apply(filename, conflict_handler, reverse)
414
class Diff3Merge(object):
415
history_based = False
416
def __init__(self, file_id, base, other):
417
self.file_id = file_id
421
def is_creation(self):
424
def is_deletion(self):
427
def __eq__(self, other):
428
if not isinstance(other, Diff3Merge):
430
return (self.base == other.base and
431
self.other == other.other and self.file_id == other.file_id)
433
def __ne__(self, other):
434
return not (self == other)
436
def dump_file(self, temp_dir, name, tree):
437
out_path = os.path.join(temp_dir, name)
438
out_file = file(out_path, "wb")
439
in_file = tree.get_file(self.file_id)
444
def apply(self, filename, conflict_handler, reverse=False):
446
temp_dir = mkdtemp(prefix="bzr-")
448
new_file = filename+".new"
449
base_file = self.dump_file(temp_dir, "base", self.base)
450
other_file = self.dump_file(temp_dir, "other", self.other)
457
status = bzrlib.patch.diff3(new_file, filename, base, other)
459
os.chmod(new_file, os.stat(filename).st_mode)
460
rename(new_file, filename)
464
def get_lines(filename):
465
my_file = file(filename, "rb")
466
lines = my_file.readlines()
469
base_lines = get_lines(base)
470
other_lines = get_lines(other)
471
conflict_handler.merge_conflict(new_file, filename, base_lines,
478
"""Convenience function to create a directory.
480
:return: A ReplaceContents that will create a directory
481
:rtype: `ReplaceContents`
483
return ReplaceContents(None, dir_create)
486
"""Convenience function to delete a directory.
488
:return: A ReplaceContents that will delete a directory
489
:rtype: `ReplaceContents`
491
return ReplaceContents(dir_create, None)
493
def CreateFile(contents):
494
"""Convenience fucntion to create a file.
496
:param contents: The contents of the file to create
498
:return: A ReplaceContents that will create a file
499
:rtype: `ReplaceContents`
501
return ReplaceContents(None, FileCreate(contents))
503
def DeleteFile(contents):
504
"""Convenience fucntion to delete a file.
506
:param contents: The contents of the file to delete
508
:return: A ReplaceContents that will delete a file
509
:rtype: `ReplaceContents`
511
return ReplaceContents(FileCreate(contents), None)
513
def ReplaceFileContents(old_tree, new_tree, file_id):
514
"""Convenience fucntion to replace the contents of a file.
516
:param old_contents: The contents of the file to replace
517
:type old_contents: str
518
:param new_contents: The contents to replace the file with
519
:type new_contents: str
520
:return: A ReplaceContents that will replace the contents of a file a file
521
:rtype: `ReplaceContents`
523
return ReplaceContents(TreeFileCreate(old_tree, file_id),
524
TreeFileCreate(new_tree, file_id))
526
def CreateSymlink(target):
527
"""Convenience fucntion to create a symlink.
529
:param target: The path the link should point to
531
:return: A ReplaceContents that will delete a file
532
:rtype: `ReplaceContents`
534
return ReplaceContents(None, SymlinkCreate(target))
536
def DeleteSymlink(target):
537
"""Convenience fucntion to delete a symlink.
539
:param target: The path the link should point to
541
:return: A ReplaceContents that will delete a file
542
:rtype: `ReplaceContents`
544
return ReplaceContents(SymlinkCreate(target), None)
546
def ChangeTarget(old_target, new_target):
547
"""Convenience fucntion to change the target of a symlink.
549
:param old_target: The current link target
550
:type old_target: str
551
:param new_target: The new link target to use
552
:type new_target: str
553
:return: A ReplaceContents that will delete a file
554
:rtype: `ReplaceContents`
556
return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
559
class InvalidEntry(Exception):
560
"""Raise when a ChangesetEntry is invalid in some way"""
561
def __init__(self, entry, problem):
564
:param entry: The invalid ChangesetEntry
565
:type entry: `ChangesetEntry`
566
:param problem: The problem with the entry
569
msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id,
572
Exception.__init__(self, msg)
576
class SourceRootHasName(InvalidEntry):
577
"""This changeset entry has a name other than "", but its parent is !NULL"""
578
def __init__(self, entry, name):
581
:param entry: The invalid ChangesetEntry
582
:type entry: `ChangesetEntry`
583
:param name: The name of the entry
586
msg = 'Child of !NULL is named "%s", not "./.".' % name
587
InvalidEntry.__init__(self, entry, msg)
589
class NullIDAssigned(InvalidEntry):
590
"""The id !NULL was assigned to a real entry"""
591
def __init__(self, entry):
594
:param entry: The invalid ChangesetEntry
595
:type entry: `ChangesetEntry`
597
msg = '"!NULL" id assigned to a file "%s".' % entry.path
598
InvalidEntry.__init__(self, entry, msg)
600
class ParentIDIsSelf(InvalidEntry):
601
"""An entry is marked as its own parent"""
602
def __init__(self, entry):
605
:param entry: The invalid ChangesetEntry
606
:type entry: `ChangesetEntry`
608
msg = 'file %s has "%s" id for both self id and parent id.' % \
609
(entry.path, entry.id)
610
InvalidEntry.__init__(self, entry, msg)
612
class ChangesetEntry(object):
613
"""An entry the changeset"""
614
def __init__(self, id, parent, path):
615
"""Constructor. Sets parent and name assuming it was not
616
renamed/created/deleted.
617
:param id: The id associated with the entry
618
:param parent: The id of the parent of this entry (or !NULL if no
620
:param path: The file path relative to the tree root of this entry
626
self.new_parent = parent
627
self.contents_change = None
628
self.metadata_change = None
629
if parent == NULL_ID and path !='./.':
630
raise SourceRootHasName(self, path)
631
if self.id == NULL_ID:
632
raise NullIDAssigned(self)
633
if self.id == self.parent:
634
raise ParentIDIsSelf(self)
637
return "ChangesetEntry(%s)" % self.id
642
if self.path is None:
644
return os.path.dirname(self.path)
646
def __set_dir(self, dir):
647
self.path = os.path.join(dir, os.path.basename(self.path))
649
dir = property(__get_dir, __set_dir)
651
def __get_name(self):
652
if self.path is None:
654
return os.path.basename(self.path)
656
def __set_name(self, name):
657
self.path = os.path.join(os.path.dirname(self.path), name)
659
name = property(__get_name, __set_name)
661
def __get_new_dir(self):
662
if self.new_path is None:
664
return os.path.dirname(self.new_path)
666
def __set_new_dir(self, dir):
667
self.new_path = os.path.join(dir, os.path.basename(self.new_path))
669
new_dir = property(__get_new_dir, __set_new_dir)
671
def __get_new_name(self):
672
if self.new_path is None:
674
return os.path.basename(self.new_path)
676
def __set_new_name(self, name):
677
self.new_path = os.path.join(os.path.dirname(self.new_path), name)
679
new_name = property(__get_new_name, __set_new_name)
681
def needs_rename(self):
682
"""Determines whether the entry requires renaming.
687
return (self.parent != self.new_parent or self.name != self.new_name)
689
def is_deletion(self, reverse):
690
"""Return true if applying the entry would delete a file/directory.
692
:param reverse: if true, the changeset is being applied in reverse
695
return self.is_creation(not reverse)
697
def is_creation(self, reverse):
698
"""Return true if applying the entry would create a file/directory.
700
:param reverse: if true, the changeset is being applied in reverse
703
if self.contents_change is None:
706
return self.contents_change.is_deletion()
708
return self.contents_change.is_creation()
710
def is_creation_or_deletion(self):
711
"""Return true if applying the entry would create or delete a
716
return self.is_creation(False) or self.is_deletion(False)
718
def get_cset_path(self, mod=False):
719
"""Determine the path of the entry according to the changeset.
721
:param changeset: The changeset to derive the path from
722
:type changeset: `Changeset`
723
:param mod: If true, generate the MOD path. Otherwise, generate the \
725
:return: the path of the entry, or None if it did not exist in the \
727
:rtype: str or NoneType
730
if self.new_parent == NULL_ID:
732
elif self.new_parent is None:
736
if self.parent == NULL_ID:
738
elif self.parent is None:
742
def summarize_name(self, reverse=False):
743
"""Produce a one-line summary of the filename. Indicates renames as
744
old => new, indicates creation as None => new, indicates deletion as
747
:param changeset: The changeset to get paths from
748
:type changeset: `Changeset`
749
:param reverse: If true, reverse the names in the output
753
orig_path = self.get_cset_path(False)
754
mod_path = self.get_cset_path(True)
755
if orig_path and orig_path.startswith('./'):
756
orig_path = orig_path[2:]
757
if mod_path and mod_path.startswith('./'):
758
mod_path = mod_path[2:]
759
if orig_path == mod_path:
763
return "%s => %s" % (orig_path, mod_path)
765
return "%s => %s" % (mod_path, orig_path)
768
def get_new_path(self, id_map, changeset, reverse=False):
769
"""Determine the full pathname to rename to
771
:param id_map: The map of ids to filenames for the tree
772
:type id_map: Dictionary
773
:param changeset: The changeset to get data from
774
:type changeset: `Changeset`
775
:param reverse: If true, we're applying the changeset in reverse
779
mutter("Finding new path for %s", self.summarize_name())
783
from_dir = self.new_dir
785
from_name = self.new_name
787
parent = self.new_parent
788
to_dir = self.new_dir
790
to_name = self.new_name
791
from_name = self.name
796
if parent == NULL_ID or parent is None:
798
raise SourceRootHasName(self, to_name)
801
parent_entry = changeset.entries.get(parent)
802
if parent_entry is None:
803
dir = os.path.dirname(id_map[self.id])
805
mutter("path, new_path: %r %r", self.path, self.new_path)
806
dir = parent_entry.get_new_path(id_map, changeset, reverse)
807
if from_name == to_name:
808
name = os.path.basename(id_map[self.id])
811
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
812
return os.path.join(dir, name)
815
"""Determines whether the entry does nothing
817
:return: True if the entry does no renames or content changes
820
if self.contents_change is not None:
822
elif self.metadata_change is not None:
824
elif self.parent != self.new_parent:
826
elif self.name != self.new_name:
831
def apply(self, filename, conflict_handler, reverse=False):
832
"""Applies the file content and/or metadata changes.
834
:param filename: the filename of the entry
836
:param reverse: If true, apply the changes in reverse
839
if self.is_deletion(reverse) and self.metadata_change is not None:
840
self.metadata_change.apply(filename, conflict_handler, reverse)
841
if self.contents_change is not None:
842
self.contents_change.apply(filename, conflict_handler, reverse)
843
if not self.is_deletion(reverse) and self.metadata_change is not None:
844
self.metadata_change.apply(filename, conflict_handler, reverse)
846
class IDPresent(Exception):
847
def __init__(self, id):
848
msg = "Cannot add entry because that id has already been used:\n%s" %\
850
Exception.__init__(self, msg)
853
class Changeset(object):
854
"""A set of changes to apply"""
858
def add_entry(self, entry):
859
"""Add an entry to the list of entries"""
860
if self.entries.has_key(entry.id):
861
raise IDPresent(entry.id)
862
self.entries[entry.id] = entry
864
def my_sort(sequence, key, reverse=False):
865
"""A sort function that supports supplying a key for comparison
867
:param sequence: The sequence to sort
868
:param key: A callable object that returns the values to be compared
869
:param reverse: If true, sort in reverse order
872
def cmp_by_key(entry_a, entry_b):
877
return cmp(key(entry_a), key(entry_b))
878
sequence.sort(cmp_by_key)
880
def get_rename_entries(changeset, inventory, reverse):
881
"""Return a list of entries that will be renamed. Entries are sorted from
882
longest to shortest source path and from shortest to longest target path.
884
:param changeset: The changeset to look in
885
:type changeset: `Changeset`
886
:param inventory: The source of current tree paths for the given ids
887
:type inventory: Dictionary
888
:param reverse: If true, the changeset is being applied in reverse
890
:return: source entries and target entries as a tuple
893
source_entries = [x for x in changeset.entries.itervalues()
894
if x.needs_rename() or x.is_creation_or_deletion()]
895
# these are done from longest path to shortest, to avoid deleting a
896
# parent before its children are deleted/renamed
897
def longest_to_shortest(entry):
898
path = inventory.get(entry.id)
903
my_sort(source_entries, longest_to_shortest, reverse=True)
905
target_entries = source_entries[:]
906
# These are done from shortest to longest path, to avoid creating a
907
# child before its parent has been created/renamed
908
def shortest_to_longest(entry):
909
path = entry.get_new_path(inventory, changeset, reverse)
914
my_sort(target_entries, shortest_to_longest)
915
return (source_entries, target_entries)
917
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
918
conflict_handler, reverse):
919
"""Delete and rename entries as appropriate. Entries are renamed to temp
920
names. A map of id -> temp name (or None, for deletions) is returned.
922
:param source_entries: The entries to rename and delete
923
:type source_entries: List of `ChangesetEntry`
924
:param inventory: The map of id -> filename in the current tree
925
:type inventory: Dictionary
926
:param dir: The directory to apply changes to
928
:param reverse: Apply changes in reverse
930
:return: a mapping of id to temporary name
934
for i in range(len(source_entries)):
935
entry = source_entries[i]
936
if entry.is_deletion(reverse):
937
path = os.path.join(dir, inventory[entry.id])
938
entry.apply(path, conflict_handler, reverse)
939
temp_name[entry.id] = None
941
elif entry.needs_rename():
942
if entry.is_creation(reverse):
944
to_name = os.path.join(temp_dir, str(i))
945
src_path = inventory.get(entry.id)
946
if src_path is not None:
947
src_path = os.path.join(dir, src_path)
949
rename(src_path, to_name)
950
temp_name[entry.id] = to_name
952
if e.errno != errno.ENOENT:
954
if conflict_handler.missing_for_rename(src_path, to_name) \
961
def rename_to_new_create(changed_inventory, target_entries, inventory,
962
changeset, dir, conflict_handler, reverse):
963
"""Rename entries with temp names to their final names, create new files.
965
:param changed_inventory: A mapping of id to temporary name
966
:type changed_inventory: Dictionary
967
:param target_entries: The entries to apply changes to
968
:type target_entries: List of `ChangesetEntry`
969
:param changeset: The changeset to apply
970
:type changeset: `Changeset`
971
:param dir: The directory to apply changes to
973
:param reverse: If true, apply changes in reverse
976
for entry in target_entries:
977
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
978
if new_tree_path is None:
980
new_path = os.path.join(dir, new_tree_path)
981
old_path = changed_inventory.get(entry.id)
982
if bzrlib.osutils.lexists(new_path):
983
if conflict_handler.target_exists(entry, new_path, old_path) == \
986
if entry.is_creation(reverse):
987
entry.apply(new_path, conflict_handler, reverse)
988
changed_inventory[entry.id] = new_tree_path
989
elif entry.needs_rename():
990
if entry.is_deletion(reverse):
995
mutter('rename %s to final name %s', old_path, new_path)
996
rename(old_path, new_path)
997
changed_inventory[entry.id] = new_tree_path
999
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
1000
% (old_path, new_path, entry, e))
1002
class TargetExists(Exception):
1003
def __init__(self, entry, target):
1004
msg = "The path %s already exists" % target
1005
Exception.__init__(self, msg)
1007
self.target = target
1009
class RenameConflict(Exception):
1010
def __init__(self, id, this_name, base_name, other_name):
1011
msg = """Trees all have different names for a file
1015
id: %s""" % (this_name, base_name, other_name, id)
1016
Exception.__init__(self, msg)
1017
self.this_name = this_name
1018
self.base_name = base_name
1019
self_other_name = other_name
1021
class MoveConflict(Exception):
1022
def __init__(self, id, this_parent, base_parent, other_parent):
1023
msg = """The file is in different directories in every tree
1027
id: %s""" % (this_parent, base_parent, other_parent, id)
1028
Exception.__init__(self, msg)
1029
self.this_parent = this_parent
1030
self.base_parent = base_parent
1031
self_other_parent = other_parent
1033
class MergeConflict(Exception):
1034
def __init__(self, this_path):
1035
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1036
self.this_path = this_path
1038
class WrongOldContents(Exception):
1039
def __init__(self, filename):
1040
msg = "Contents mismatch deleting %s" % filename
1041
self.filename = filename
1042
Exception.__init__(self, msg)
1044
class WrongOldExecFlag(Exception):
1045
def __init__(self, filename, old_exec_flag, new_exec_flag):
1046
msg = "Executable flag missmatch on %s:\n" \
1047
"Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
1048
self.filename = filename
1049
Exception.__init__(self, msg)
1051
class RemoveContentsConflict(Exception):
1052
def __init__(self, filename):
1053
msg = "Conflict deleting %s, which has different contents in BASE"\
1054
" and THIS" % filename
1055
self.filename = filename
1056
Exception.__init__(self, msg)
1058
class DeletingNonEmptyDirectory(Exception):
1059
def __init__(self, filename):
1060
msg = "Trying to remove dir %s while it still had files" % filename
1061
self.filename = filename
1062
Exception.__init__(self, msg)
1065
class PatchTargetMissing(Exception):
1066
def __init__(self, filename):
1067
msg = "Attempt to patch %s, which does not exist" % filename
1068
Exception.__init__(self, msg)
1069
self.filename = filename
1071
class MissingForSetExec(Exception):
1072
def __init__(self, filename):
1073
msg = "Attempt to change permissions on %s, which does not exist" %\
1075
Exception.__init__(self, msg)
1076
self.filename = filename
1078
class MissingForRm(Exception):
1079
def __init__(self, filename):
1080
msg = "Attempt to remove missing path %s" % filename
1081
Exception.__init__(self, msg)
1082
self.filename = filename
1085
class MissingForRename(Exception):
1086
def __init__(self, filename, to_path):
1087
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1088
Exception.__init__(self, msg)
1089
self.filename = filename
1091
class NewContentsConflict(Exception):
1092
def __init__(self, filename):
1093
msg = "Conflicting contents for new file %s" % (filename)
1094
Exception.__init__(self, msg)
1096
class WeaveMergeConflict(Exception):
1097
def __init__(self, filename):
1098
msg = "Conflicting contents for file %s" % (filename)
1099
Exception.__init__(self, msg)
1101
class ThreewayContentsConflict(Exception):
1102
def __init__(self, filename):
1103
msg = "Conflicting contents for file %s" % (filename)
1104
Exception.__init__(self, msg)
1107
class MissingForMerge(Exception):
1108
def __init__(self, filename):
1109
msg = "The file %s was modified, but does not exist in this tree"\
1111
Exception.__init__(self, msg)
1114
class ExceptionConflictHandler(object):
1115
"""Default handler for merge exceptions.
1117
This throws an error on any kind of conflict. Conflict handlers can
1118
descend from this class if they have a better way to handle some or
1119
all types of conflict.
1121
def missing_parent(self, pathname):
1122
parent = os.path.dirname(pathname)
1123
raise Exception("Parent directory missing for %s" % pathname)
1125
def dir_exists(self, pathname):
1126
raise Exception("Directory already exists for %s" % pathname)
1128
def failed_hunks(self, pathname):
1129
raise Exception("Failed to apply some hunks for %s" % pathname)
1131
def target_exists(self, entry, target, old_path):
1132
raise TargetExists(entry, target)
1134
def rename_conflict(self, id, this_name, base_name, other_name):
1135
raise RenameConflict(id, this_name, base_name, other_name)
1137
def move_conflict(self, id, this_dir, base_dir, other_dir):
1138
raise MoveConflict(id, this_dir, base_dir, other_dir)
1140
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1142
raise MergeConflict(this_path)
1144
def wrong_old_contents(self, filename, expected_contents):
1145
raise WrongOldContents(filename)
1147
def rem_contents_conflict(self, filename, this_contents, base_contents):
1148
raise RemoveContentsConflict(filename)
1150
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1151
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1153
def rmdir_non_empty(self, filename):
1154
raise DeletingNonEmptyDirectory(filename)
1156
def link_name_exists(self, filename):
1157
raise TargetExists(filename)
1159
def patch_target_missing(self, filename, contents):
1160
raise PatchTargetMissing(filename)
1162
def missing_for_exec_flag(self, filename):
1163
raise MissingForExecFlag(filename)
1165
def missing_for_rm(self, filename, change):
1166
raise MissingForRm(filename)
1168
def missing_for_rename(self, filename, to_path):
1169
raise MissingForRename(filename, to_path)
1171
def missing_for_merge(self, file_id, other_path):
1172
raise MissingForMerge(other_path)
1174
def new_contents_conflict(self, filename, other_contents):
1175
raise NewContentsConflict(filename)
1177
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1178
raise WeaveMergeConflict(filename)
1180
def threeway_contents_conflict(self, filename, this_contents,
1181
base_contents, other_contents):
1182
raise ThreewayContentsConflict(filename)
1187
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1189
"""Apply a changeset to a directory.
1191
:param changeset: The changes to perform
1192
:type changeset: `Changeset`
1193
:param inventory: The mapping of id to filename for the directory
1194
:type inventory: Dictionary
1195
:param dir: The path of the directory to apply the changes to
1197
:param reverse: If true, apply the changes in reverse
1199
:return: The mapping of the changed entries
1202
if conflict_handler is None:
1203
conflict_handler = ExceptionConflictHandler()
1204
temp_dir = os.path.join(dir, "bzr-tree-change")
1208
if e.errno == errno.EEXIST:
1212
if e.errno == errno.ENOTEMPTY:
1213
raise OldFailedTreeOp()
1218
#apply changes that don't affect filenames
1219
for entry in changeset.entries.itervalues():
1220
if not entry.is_creation_or_deletion() and not entry.is_boring():
1221
if entry.id not in inventory:
1222
warning("entry {%s} no longer present, can't be updated",
1225
path = os.path.join(dir, inventory[entry.id])
1226
entry.apply(path, conflict_handler, reverse)
1228
# Apply renames in stages, to minimize conflicts:
1229
# Only files whose name or parent change are interesting, because their
1230
# target name may exist in the source tree. If a directory's name changes,
1231
# that doesn't make its children interesting.
1232
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1235
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1236
temp_dir, conflict_handler,
1239
rename_to_new_create(changed_inventory, target_entries, inventory,
1240
changeset, dir, conflict_handler, reverse)
1242
return changed_inventory
1245
def apply_changeset_tree(cset, tree, reverse=False):
1247
for entry in tree.source_inventory().itervalues():
1248
inventory[entry.id] = entry.path
1249
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1251
new_entries, remove_entries = \
1252
get_inventory_change(inventory, new_inventory, cset, reverse)
1253
tree.update_source_inventory(new_entries, remove_entries)
1256
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1259
for entry in cset.entries.itervalues():
1260
if entry.needs_rename():
1261
new_path = entry.get_new_path(inventory, cset)
1262
if new_path is None:
1263
remove_entries.append(entry.id)
1265
new_entries[new_path] = entry.id
1266
return new_entries, remove_entries
1269
def print_changeset(cset):
1270
"""Print all non-boring changeset entries
1272
:param cset: The changeset to print
1273
:type cset: `Changeset`
1275
for entry in cset.entries.itervalues():
1276
if entry.is_boring():
1279
print entry.summarize_name(cset)
1281
class CompositionFailure(Exception):
1282
def __init__(self, old_entry, new_entry, problem):
1283
msg = "Unable to conpose entries.\n %s" % problem
1284
Exception.__init__(self, msg)
1286
class IDMismatch(CompositionFailure):
1287
def __init__(self, old_entry, new_entry):
1288
problem = "Attempt to compose entries with different ids: %s and %s" %\
1289
(old_entry.id, new_entry.id)
1290
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1292
def compose_changesets(old_cset, new_cset):
1293
"""Combine two changesets into one. This works well for exact patching.
1294
Otherwise, not so well.
1296
:param old_cset: The first changeset that would be applied
1297
:type old_cset: `Changeset`
1298
:param new_cset: The second changeset that would be applied
1299
:type new_cset: `Changeset`
1300
:return: A changeset that combines the changes in both changesets
1303
composed = Changeset()
1304
for old_entry in old_cset.entries.itervalues():
1305
new_entry = new_cset.entries.get(old_entry.id)
1306
if new_entry is None:
1307
composed.add_entry(old_entry)
1309
composed_entry = compose_entries(old_entry, new_entry)
1310
if composed_entry.parent is not None or\
1311
composed_entry.new_parent is not None:
1312
composed.add_entry(composed_entry)
1313
for new_entry in new_cset.entries.itervalues():
1314
if not old_cset.entries.has_key(new_entry.id):
1315
composed.add_entry(new_entry)
1318
def compose_entries(old_entry, new_entry):
1319
"""Combine two entries into one.
1321
:param old_entry: The first entry that would be applied
1322
:type old_entry: ChangesetEntry
1323
:param old_entry: The second entry that would be applied
1324
:type old_entry: ChangesetEntry
1325
:return: A changeset entry combining both entries
1326
:rtype: `ChangesetEntry`
1328
if old_entry.id != new_entry.id:
1329
raise IDMismatch(old_entry, new_entry)
1330
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1332
if (old_entry.parent != old_entry.new_parent or
1333
new_entry.parent != new_entry.new_parent):
1334
output.new_parent = new_entry.new_parent
1336
if (old_entry.path != old_entry.new_path or
1337
new_entry.path != new_entry.new_path):
1338
output.new_path = new_entry.new_path
1340
output.contents_change = compose_contents(old_entry, new_entry)
1341
output.metadata_change = compose_metadata(old_entry, new_entry)
1344
def compose_contents(old_entry, new_entry):
1345
"""Combine the contents of two changeset entries. Entries are combined
1346
intelligently where possible, but the fallback behavior returns an
1349
:param old_entry: The first entry that would be applied
1350
:type old_entry: `ChangesetEntry`
1351
:param new_entry: The second entry that would be applied
1352
:type new_entry: `ChangesetEntry`
1353
:return: A combined contents change
1354
:rtype: anything supporting the apply(reverse=False) method
1356
old_contents = old_entry.contents_change
1357
new_contents = new_entry.contents_change
1358
if old_entry.contents_change is None:
1359
return new_entry.contents_change
1360
elif new_entry.contents_change is None:
1361
return old_entry.contents_change
1362
elif isinstance(old_contents, ReplaceContents) and \
1363
isinstance(new_contents, ReplaceContents):
1364
if old_contents.old_contents == new_contents.new_contents:
1367
return ReplaceContents(old_contents.old_contents,
1368
new_contents.new_contents)
1369
elif isinstance(old_contents, ApplySequence):
1370
output = ApplySequence(old_contents.changes)
1371
if isinstance(new_contents, ApplySequence):
1372
output.changes.extend(new_contents.changes)
1374
output.changes.append(new_contents)
1376
elif isinstance(new_contents, ApplySequence):
1377
output = ApplySequence((old_contents.changes,))
1378
output.extend(new_contents.changes)
1381
return ApplySequence((old_contents, new_contents))
1383
def compose_metadata(old_entry, new_entry):
1384
old_meta = old_entry.metadata_change
1385
new_meta = new_entry.metadata_change
1386
if old_meta is None:
1388
elif new_meta is None:
1390
elif (isinstance(old_meta, ChangeExecFlag) and
1391
isinstance(new_meta, ChangeExecFlag)):
1392
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1394
return ApplySequence(old_meta, new_meta)
1397
def changeset_is_null(changeset):
1398
for entry in changeset.entries.itervalues():
1399
if not entry.is_boring():
1403
class UnsupportedFiletype(Exception):
1404
def __init__(self, kind, full_path):
1405
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1407
Exception.__init__(self, msg)
1408
self.full_path = full_path
1411
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1412
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1415
class ChangesetGenerator(object):
1416
def __init__(self, tree_a, tree_b, interesting_ids=None):
1417
object.__init__(self)
1418
self.tree_a = tree_a
1419
self.tree_b = tree_b
1420
self._interesting_ids = interesting_ids
1422
def iter_both_tree_ids(self):
1423
for file_id in self.tree_a:
1425
for file_id in self.tree_b:
1426
if file_id not in self.tree_a:
1431
for file_id in self.iter_both_tree_ids():
1432
cs_entry = self.make_entry(file_id)
1433
if cs_entry is not None and not cs_entry.is_boring():
1434
cset.add_entry(cs_entry)
1436
for entry in list(cset.entries.itervalues()):
1437
if entry.parent != entry.new_parent:
1438
if not cset.entries.has_key(entry.parent) and\
1439
entry.parent != NULL_ID and entry.parent is not None:
1440
parent_entry = self.make_boring_entry(entry.parent)
1441
cset.add_entry(parent_entry)
1442
if not cset.entries.has_key(entry.new_parent) and\
1443
entry.new_parent != NULL_ID and \
1444
entry.new_parent is not None:
1445
parent_entry = self.make_boring_entry(entry.new_parent)
1446
cset.add_entry(parent_entry)
1449
def iter_inventory(self, tree):
1450
for file_id in tree:
1451
yield self.get_entry(file_id, tree)
1453
def get_entry(self, file_id, tree):
1454
if not tree.has_or_had_id(file_id):
1456
return tree.inventory[file_id]
1458
def get_entry_parent(self, entry):
1461
return entry.parent_id
1463
def get_path(self, file_id, tree):
1464
if not tree.has_or_had_id(file_id):
1466
path = tree.id2path(file_id)
1472
def make_basic_entry(self, file_id, only_interesting):
1473
entry_a = self.get_entry(file_id, self.tree_a)
1474
entry_b = self.get_entry(file_id, self.tree_b)
1475
if only_interesting and not self.is_interesting(entry_a, entry_b):
1477
parent = self.get_entry_parent(entry_a)
1478
path = self.get_path(file_id, self.tree_a)
1479
cs_entry = ChangesetEntry(file_id, parent, path)
1480
new_parent = self.get_entry_parent(entry_b)
1482
new_path = self.get_path(file_id, self.tree_b)
1484
cs_entry.new_path = new_path
1485
cs_entry.new_parent = new_parent
1488
def is_interesting(self, entry_a, entry_b):
1489
if self._interesting_ids is None:
1491
if entry_a is not None:
1492
file_id = entry_a.file_id
1493
elif entry_b is not None:
1494
file_id = entry_b.file_id
1497
return file_id in self._interesting_ids
1499
def make_boring_entry(self, id):
1500
cs_entry = self.make_basic_entry(id, only_interesting=False)
1501
if cs_entry.is_creation_or_deletion():
1502
return self.make_entry(id, only_interesting=False)
1507
def make_entry(self, id, only_interesting=True):
1508
cs_entry = self.make_basic_entry(id, only_interesting)
1510
if cs_entry is None:
1513
cs_entry.metadata_change = self.make_exec_flag_change(id)
1515
if id in self.tree_a and id in self.tree_b:
1516
a_sha1 = self.tree_a.get_file_sha1(id)
1517
b_sha1 = self.tree_b.get_file_sha1(id)
1518
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1521
cs_entry.contents_change = self.make_contents_change(id)
1524
def make_exec_flag_change(self, file_id):
1525
exec_flag_a = exec_flag_b = None
1526
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1527
exec_flag_a = self.tree_a.is_executable(file_id)
1529
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1530
exec_flag_b = self.tree_b.is_executable(file_id)
1532
if exec_flag_a == exec_flag_b:
1534
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1536
def make_contents_change(self, file_id):
1537
a_contents = get_contents(self.tree_a, file_id)
1538
b_contents = get_contents(self.tree_b, file_id)
1539
if a_contents == b_contents:
1541
return ReplaceContents(a_contents, b_contents)
1544
def get_contents(tree, file_id):
1545
"""Return the appropriate contents to create a copy of file_id from tree"""
1546
if file_id not in tree:
1548
kind = tree.kind(file_id)
1550
return TreeFileCreate(tree, file_id)
1551
elif kind in ("directory", "root_directory"):
1553
elif kind == "symlink":
1554
return SymlinkCreate(tree.get_symlink_target(file_id))
1556
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1559
def full_path(entry, tree):
1560
return os.path.join(tree.basedir, entry.path)
1562
def new_delete_entry(entry, tree, inventory, delete):
1563
if entry.path == "":
1566
parent = inventory[dirname(entry.path)].id
1567
cs_entry = ChangesetEntry(parent, entry.path)
1569
cs_entry.new_path = None
1570
cs_entry.new_parent = None
1572
cs_entry.path = None
1573
cs_entry.parent = None
1574
full_path = full_path(entry, tree)
1575
status = os.lstat(full_path)
1576
if stat.S_ISDIR(file_stat.st_mode):
1582
# XXX: Can't we unify this with the regular inventory object
1583
class Inventory(object):
1584
def __init__(self, inventory):
1585
self.inventory = inventory
1586
self.rinventory = None
1588
def get_rinventory(self):
1589
if self.rinventory is None:
1590
self.rinventory = invert_dict(self.inventory)
1591
return self.rinventory
1593
def get_path(self, id):
1594
return self.inventory.get(id)
1596
def get_name(self, id):
1597
path = self.get_path(id)
1601
return os.path.basename(path)
1603
def get_dir(self, id):
1604
path = self.get_path(id)
1609
return os.path.dirname(path)
1611
def get_parent(self, id):
1612
if self.get_path(id) is None:
1614
directory = self.get_dir(id)
1615
if directory == '.':
1617
if directory is None:
1619
return self.get_rinventory().get(directory)