1
# Copyright (C) 2005-2010 Canonical Ltd
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from cStringIO import StringIO
19
from bzrlib.lazy_import import lazy_import
20
lazy_import(globals(), """
25
from bzrlib.bundle import serializer as _serializer
26
from bzrlib.merge_directive import MergeDirective
27
from bzrlib.transport import (
28
do_catching_redirections,
32
from bzrlib.trace import note
35
def read_mergeable_from_url(url, _do_directive=True, possible_transports=None):
36
"""Read mergable object from a given URL.
38
:return: An object supporting get_target_revision. Raises NotABundle if
39
the target is not a mergeable type.
41
child_transport = get_transport(url,
42
possible_transports=possible_transports)
43
transport = child_transport.clone('..')
44
filename = transport.relpath(child_transport.base)
45
mergeable, transport = read_mergeable_from_transport(transport, filename,
50
def read_mergeable_from_transport(transport, filename, _do_directive=True):
51
def get_bundle(transport):
52
return StringIO(transport.get_bytes(filename)), transport
54
def redirected_transport(transport, exception, redirection_notice):
55
note(redirection_notice)
56
url, filename = urlutils.split(exception.target,
57
exclude_trailing_slash=False)
59
raise errors.NotABundle('A directory cannot be a bundle')
60
return get_transport(url)
1
# Copyright (C) 2004 Aaron Bentley <aaron.bentley@utoronto.ca>
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Represent and apply a changeset.
19
Conflicts in applying a changeset are represented as exceptions.
21
This only handles the in-memory objects representing changesets, which are
22
primarily used by the merge code.
28
from tempfile import mkdtemp
29
from shutil import rmtree
30
from itertools import izip
32
from bzrlib.trace import mutter, warning
33
from bzrlib.osutils import rename, sha_file
36
__docformat__ = "restructuredtext"
40
class OldFailedTreeOp(Exception):
42
Exception.__init__(self, "bzr-tree-change contains files from a"
43
" previous failed merge operation.")
44
def invert_dict(dict):
46
for (key,value) in dict.iteritems():
51
class ChangeExecFlag(object):
52
"""This is two-way change, suitable for file modification, creation,
54
def __init__(self, old_exec_flag, new_exec_flag):
55
self.old_exec_flag = old_exec_flag
56
self.new_exec_flag = new_exec_flag
58
def apply(self, filename, conflict_handler, reverse=False):
60
from_exec_flag = self.old_exec_flag
61
to_exec_flag = self.new_exec_flag
63
from_exec_flag = self.new_exec_flag
64
to_exec_flag = self.old_exec_flag
66
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
68
if e.errno == errno.ENOENT:
69
if conflict_handler.missing_for_exec_flag(filename) == "skip":
72
current_exec_flag = from_exec_flag
74
if from_exec_flag is not None and current_exec_flag != from_exec_flag:
75
if conflict_handler.wrong_old_exec_flag(filename,
76
from_exec_flag, current_exec_flag) != "continue":
79
if to_exec_flag is not None:
80
current_mode = os.stat(filename).st_mode
84
to_mode = current_mode | (0100 & ~umask)
85
# Enable x-bit for others only if they can read it.
86
if current_mode & 0004:
87
to_mode |= 0001 & ~umask
88
if current_mode & 0040:
89
to_mode |= 0010 & ~umask
91
to_mode = current_mode & ~0111
93
os.chmod(filename, to_mode)
95
if e.errno == errno.ENOENT:
96
conflict_handler.missing_for_exec_flag(filename)
98
def __eq__(self, other):
99
return (isinstance(other, ChangeExecFlag) and
100
self.old_exec_flag == other.old_exec_flag and
101
self.new_exec_flag == other.new_exec_flag)
103
def __ne__(self, other):
104
return not (self == other)
107
def dir_create(filename, conflict_handler, reverse):
108
"""Creates the directory, or deletes it if reverse is true. Intended to be
109
used with ReplaceContents.
111
:param filename: The name of the directory to create
113
:param reverse: If true, delete the directory, instead
120
if e.errno != errno.EEXIST:
122
if conflict_handler.dir_exists(filename) == "continue":
125
if e.errno == errno.ENOENT:
126
if conflict_handler.missing_parent(filename)=="continue":
127
file(filename, "wb").write(self.contents)
132
if e.errno != errno.ENOTEMPTY:
134
if conflict_handler.rmdir_non_empty(filename) == "skip":
139
class SymlinkCreate(object):
140
"""Creates or deletes a symlink (for use with ReplaceContents)"""
141
def __init__(self, contents):
144
:param contents: The filename of the target the symlink should point to
147
self.target = contents
150
return "SymlinkCreate(%s)" % self.target
152
def __call__(self, filename, conflict_handler, reverse):
153
"""Creates or destroys the symlink.
155
:param filename: The name of the symlink to create
159
assert(os.readlink(filename) == self.target)
163
os.symlink(self.target, filename)
165
if e.errno != errno.EEXIST:
167
if conflict_handler.link_name_exists(filename) == "continue":
168
os.symlink(self.target, filename)
170
def __eq__(self, other):
171
if not isinstance(other, SymlinkCreate):
173
elif self.target != other.target:
178
def __ne__(self, other):
179
return not (self == other)
181
class FileCreate(object):
182
"""Create or delete a file (for use with ReplaceContents)"""
183
def __init__(self, contents):
186
:param contents: The contents of the file to write
189
self.contents = contents
192
return "FileCreate(%i b)" % len(self.contents)
194
def __eq__(self, other):
195
if not isinstance(other, FileCreate):
197
elif self.contents != other.contents:
202
def __ne__(self, other):
203
return not (self == other)
205
def __call__(self, filename, conflict_handler, reverse):
206
"""Create or delete a file
208
:param filename: The name of the file to create
210
:param reverse: Delete the file instead of creating it
215
file(filename, "wb").write(self.contents)
217
if e.errno == errno.ENOENT:
218
if conflict_handler.missing_parent(filename)=="continue":
219
file(filename, "wb").write(self.contents)
225
if (file(filename, "rb").read() != self.contents):
226
direction = conflict_handler.wrong_old_contents(filename,
228
if direction != "continue":
232
if e.errno != errno.ENOENT:
234
if conflict_handler.missing_for_rm(filename, undo) == "skip":
239
class TreeFileCreate(object):
240
"""Create or delete a file (for use with ReplaceContents)"""
241
def __init__(self, tree, file_id):
244
:param contents: The contents of the file to write
248
self.file_id = file_id
251
return "TreeFileCreate(%s)" % self.file_id
253
def __eq__(self, other):
254
if not isinstance(other, TreeFileCreate):
256
return self.tree.get_file_sha1(self.file_id) == \
257
other.tree.get_file_sha1(other.file_id)
259
def __ne__(self, other):
260
return not (self == other)
262
def write_file(self, filename):
263
outfile = file(filename, "wb")
264
for line in self.tree.get_file(self.file_id):
267
def same_text(self, filename):
268
in_file = file(filename, "rb")
269
return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
271
def __call__(self, filename, conflict_handler, reverse):
272
"""Create or delete a file
274
:param filename: The name of the file to create
276
:param reverse: Delete the file instead of creating it
281
self.write_file(filename)
283
if e.errno == errno.ENOENT:
284
if conflict_handler.missing_parent(filename)=="continue":
285
self.write_file(filename)
291
if not self.same_text(filename):
292
direction = conflict_handler.wrong_old_contents(filename,
293
self.tree.get_file(self.file_id).read())
294
if direction != "continue":
298
if e.errno != errno.ENOENT:
300
if conflict_handler.missing_for_rm(filename, undo) == "skip":
305
def reversed(sequence):
306
max = len(sequence) - 1
307
for i in range(len(sequence)):
308
yield sequence[max - i]
310
class ReplaceContents(object):
311
"""A contents-replacement framework. It allows a file/directory/symlink to
312
be created, deleted, or replaced with another file/directory/symlink.
313
Arguments must be callable with (filename, reverse).
315
def __init__(self, old_contents, new_contents):
318
:param old_contents: The change to reverse apply (e.g. a deletion), \
320
:type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
322
:param new_contents: The second change to apply (e.g. a creation), \
324
:type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
327
self.old_contents=old_contents
328
self.new_contents=new_contents
331
return "ReplaceContents(%r -> %r)" % (self.old_contents,
334
def __eq__(self, other):
335
if not isinstance(other, ReplaceContents):
337
elif self.old_contents != other.old_contents:
339
elif self.new_contents != other.new_contents:
343
def __ne__(self, other):
344
return not (self == other)
346
def apply(self, filename, conflict_handler, reverse=False):
347
"""Applies the FileReplacement to the specified filename
349
:param filename: The name of the file to apply changes to
351
:param reverse: If true, apply the change in reverse
355
undo = self.old_contents
356
perform = self.new_contents
358
undo = self.new_contents
359
perform = self.old_contents
363
mode = os.lstat(filename).st_mode
364
if stat.S_ISLNK(mode):
367
if e.errno != errno.ENOENT:
369
if conflict_handler.missing_for_rm(filename, undo) == "skip":
371
undo(filename, conflict_handler, reverse=True)
372
if perform is not None:
373
perform(filename, conflict_handler, reverse=False)
375
os.chmod(filename, mode)
377
def is_creation(self):
378
return self.new_contents is not None and self.old_contents is None
380
def is_deletion(self):
381
return self.old_contents is not None and self.new_contents is None
383
class ApplySequence(object):
384
def __init__(self, changes=None):
386
if changes is not None:
387
self.changes.extend(changes)
389
def __eq__(self, other):
390
if not isinstance(other, ApplySequence):
392
elif len(other.changes) != len(self.changes):
395
for i in range(len(self.changes)):
396
if self.changes[i] != other.changes[i]:
400
def __ne__(self, other):
401
return not (self == other)
404
def apply(self, filename, conflict_handler, reverse=False):
408
iter = reversed(self.changes)
410
change.apply(filename, conflict_handler, reverse)
413
class Diff3Merge(object):
414
history_based = False
415
def __init__(self, file_id, base, other):
416
self.file_id = file_id
420
def is_creation(self):
423
def is_deletion(self):
426
def __eq__(self, other):
427
if not isinstance(other, Diff3Merge):
429
return (self.base == other.base and
430
self.other == other.other and self.file_id == other.file_id)
432
def __ne__(self, other):
433
return not (self == other)
435
def dump_file(self, temp_dir, name, tree):
436
out_path = os.path.join(temp_dir, name)
437
out_file = file(out_path, "wb")
438
in_file = tree.get_file(self.file_id)
443
def apply(self, filename, conflict_handler, reverse=False):
445
temp_dir = mkdtemp(prefix="bzr-")
447
new_file = filename+".new"
448
base_file = self.dump_file(temp_dir, "base", self.base)
449
other_file = self.dump_file(temp_dir, "other", self.other)
456
status = bzrlib.patch.diff3(new_file, filename, base, other)
458
os.chmod(new_file, os.stat(filename).st_mode)
459
rename(new_file, filename)
463
def get_lines(filename):
464
my_file = file(filename, "rb")
465
lines = my_file.readlines()
468
base_lines = get_lines(base)
469
other_lines = get_lines(other)
470
conflict_handler.merge_conflict(new_file, filename, base_lines,
477
"""Convenience function to create a directory.
479
:return: A ReplaceContents that will create a directory
480
:rtype: `ReplaceContents`
482
return ReplaceContents(None, dir_create)
485
"""Convenience function to delete a directory.
487
:return: A ReplaceContents that will delete a directory
488
:rtype: `ReplaceContents`
490
return ReplaceContents(dir_create, None)
492
def CreateFile(contents):
493
"""Convenience fucntion to create a file.
495
:param contents: The contents of the file to create
497
:return: A ReplaceContents that will create a file
498
:rtype: `ReplaceContents`
500
return ReplaceContents(None, FileCreate(contents))
502
def DeleteFile(contents):
503
"""Convenience fucntion to delete a file.
505
:param contents: The contents of the file to delete
507
:return: A ReplaceContents that will delete a file
508
:rtype: `ReplaceContents`
510
return ReplaceContents(FileCreate(contents), None)
512
def ReplaceFileContents(old_tree, new_tree, file_id):
513
"""Convenience fucntion to replace the contents of a file.
515
:param old_contents: The contents of the file to replace
516
:type old_contents: str
517
:param new_contents: The contents to replace the file with
518
:type new_contents: str
519
:return: A ReplaceContents that will replace the contents of a file a file
520
:rtype: `ReplaceContents`
522
return ReplaceContents(TreeFileCreate(old_tree, file_id),
523
TreeFileCreate(new_tree, file_id))
525
def CreateSymlink(target):
526
"""Convenience fucntion to create a symlink.
528
:param target: The path the link should point to
530
:return: A ReplaceContents that will delete a file
531
:rtype: `ReplaceContents`
533
return ReplaceContents(None, SymlinkCreate(target))
535
def DeleteSymlink(target):
536
"""Convenience fucntion to delete a symlink.
538
:param target: The path the link should point to
540
:return: A ReplaceContents that will delete a file
541
:rtype: `ReplaceContents`
543
return ReplaceContents(SymlinkCreate(target), None)
545
def ChangeTarget(old_target, new_target):
546
"""Convenience fucntion to change the target of a symlink.
548
:param old_target: The current link target
549
:type old_target: str
550
:param new_target: The new link target to use
551
:type new_target: str
552
:return: A ReplaceContents that will delete a file
553
:rtype: `ReplaceContents`
555
return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
558
class InvalidEntry(Exception):
559
"""Raise when a ChangesetEntry is invalid in some way"""
560
def __init__(self, entry, problem):
563
:param entry: The invalid ChangesetEntry
564
:type entry: `ChangesetEntry`
565
:param problem: The problem with the entry
568
msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id,
571
Exception.__init__(self, msg)
575
class SourceRootHasName(InvalidEntry):
576
"""This changeset entry has a name other than "", but its parent is !NULL"""
577
def __init__(self, entry, name):
580
:param entry: The invalid ChangesetEntry
581
:type entry: `ChangesetEntry`
582
:param name: The name of the entry
585
msg = 'Child of !NULL is named "%s", not "./.".' % name
586
InvalidEntry.__init__(self, entry, msg)
588
class NullIDAssigned(InvalidEntry):
589
"""The id !NULL was assigned to a real entry"""
590
def __init__(self, entry):
593
:param entry: The invalid ChangesetEntry
594
:type entry: `ChangesetEntry`
596
msg = '"!NULL" id assigned to a file "%s".' % entry.path
597
InvalidEntry.__init__(self, entry, msg)
599
class ParentIDIsSelf(InvalidEntry):
600
"""An entry is marked as its own parent"""
601
def __init__(self, entry):
604
:param entry: The invalid ChangesetEntry
605
:type entry: `ChangesetEntry`
607
msg = 'file %s has "%s" id for both self id and parent id.' % \
608
(entry.path, entry.id)
609
InvalidEntry.__init__(self, entry, msg)
611
class ChangesetEntry(object):
612
"""An entry the changeset"""
613
def __init__(self, id, parent, path):
614
"""Constructor. Sets parent and name assuming it was not
615
renamed/created/deleted.
616
:param id: The id associated with the entry
617
:param parent: The id of the parent of this entry (or !NULL if no
619
:param path: The file path relative to the tree root of this entry
625
self.new_parent = parent
626
self.contents_change = None
627
self.metadata_change = None
628
if parent == NULL_ID and path !='./.':
629
raise SourceRootHasName(self, path)
630
if self.id == NULL_ID:
631
raise NullIDAssigned(self)
632
if self.id == self.parent:
633
raise ParentIDIsSelf(self)
636
return "ChangesetEntry(%s)" % self.id
639
if self.path is None:
641
return os.path.dirname(self.path)
643
def __set_dir(self, dir):
644
self.path = os.path.join(dir, os.path.basename(self.path))
646
dir = property(__get_dir, __set_dir)
648
def __get_name(self):
649
if self.path is None:
651
return os.path.basename(self.path)
653
def __set_name(self, name):
654
self.path = os.path.join(os.path.dirname(self.path), name)
656
name = property(__get_name, __set_name)
658
def __get_new_dir(self):
659
if self.new_path is None:
661
return os.path.dirname(self.new_path)
663
def __set_new_dir(self, dir):
664
self.new_path = os.path.join(dir, os.path.basename(self.new_path))
666
new_dir = property(__get_new_dir, __set_new_dir)
668
def __get_new_name(self):
669
if self.new_path is None:
671
return os.path.basename(self.new_path)
673
def __set_new_name(self, name):
674
self.new_path = os.path.join(os.path.dirname(self.new_path), name)
676
new_name = property(__get_new_name, __set_new_name)
678
def needs_rename(self):
679
"""Determines whether the entry requires renaming.
684
return (self.parent != self.new_parent or self.name != self.new_name)
686
def is_deletion(self, reverse):
687
"""Return true if applying the entry would delete a file/directory.
689
:param reverse: if true, the changeset is being applied in reverse
692
return self.is_creation(not reverse)
694
def is_creation(self, reverse):
695
"""Return true if applying the entry would create a file/directory.
697
:param reverse: if true, the changeset is being applied in reverse
700
if self.contents_change is None:
703
return self.contents_change.is_deletion()
705
return self.contents_change.is_creation()
707
def is_creation_or_deletion(self):
708
"""Return true if applying the entry would create or delete a
713
return self.is_creation(False) or self.is_deletion(False)
715
def get_cset_path(self, mod=False):
716
"""Determine the path of the entry according to the changeset.
718
:param changeset: The changeset to derive the path from
719
:type changeset: `Changeset`
720
:param mod: If true, generate the MOD path. Otherwise, generate the \
722
:return: the path of the entry, or None if it did not exist in the \
724
:rtype: str or NoneType
727
if self.new_parent == NULL_ID:
729
elif self.new_parent is None:
733
if self.parent == NULL_ID:
735
elif self.parent is None:
739
def summarize_name(self, reverse=False):
740
"""Produce a one-line summary of the filename. Indicates renames as
741
old => new, indicates creation as None => new, indicates deletion as
744
:param changeset: The changeset to get paths from
745
:type changeset: `Changeset`
746
:param reverse: If true, reverse the names in the output
750
orig_path = self.get_cset_path(False)
751
mod_path = self.get_cset_path(True)
752
if orig_path is not None:
753
orig_path = orig_path[2:]
754
if mod_path is not None:
755
mod_path = mod_path[2:]
756
if orig_path == mod_path:
760
return "%s => %s" % (orig_path, mod_path)
762
return "%s => %s" % (mod_path, orig_path)
765
def get_new_path(self, id_map, changeset, reverse=False):
766
"""Determine the full pathname to rename to
768
:param id_map: The map of ids to filenames for the tree
769
:type id_map: Dictionary
770
:param changeset: The changeset to get data from
771
:type changeset: `Changeset`
772
:param reverse: If true, we're applying the changeset in reverse
776
mutter("Finding new path for %s", self.summarize_name())
780
from_dir = self.new_dir
782
from_name = self.new_name
784
parent = self.new_parent
785
to_dir = self.new_dir
787
to_name = self.new_name
788
from_name = self.name
793
if parent == NULL_ID or parent is None:
795
raise SourceRootHasName(self, to_name)
798
if from_dir == to_dir:
799
dir = os.path.dirname(id_map[self.id])
801
mutter("path, new_path: %r %r", self.path, self.new_path)
802
parent_entry = changeset.entries[parent]
803
dir = parent_entry.get_new_path(id_map, changeset, reverse)
804
if from_name == to_name:
805
name = os.path.basename(id_map[self.id])
808
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
809
return os.path.join(dir, name)
812
"""Determines whether the entry does nothing
814
:return: True if the entry does no renames or content changes
817
if self.contents_change is not None:
819
elif self.metadata_change is not None:
821
elif self.parent != self.new_parent:
823
elif self.name != self.new_name:
828
def apply(self, filename, conflict_handler, reverse=False):
829
"""Applies the file content and/or metadata changes.
831
:param filename: the filename of the entry
833
:param reverse: If true, apply the changes in reverse
836
if self.is_deletion(reverse) and self.metadata_change is not None:
837
self.metadata_change.apply(filename, conflict_handler, reverse)
838
if self.contents_change is not None:
839
self.contents_change.apply(filename, conflict_handler, reverse)
840
if not self.is_deletion(reverse) and self.metadata_change is not None:
841
self.metadata_change.apply(filename, conflict_handler, reverse)
843
class IDPresent(Exception):
844
def __init__(self, id):
845
msg = "Cannot add entry because that id has already been used:\n%s" %\
847
Exception.__init__(self, msg)
850
class Changeset(object):
851
"""A set of changes to apply"""
852
def __init__(self, base_id=None, target_id=None):
853
self.base_id = base_id
854
self.target_id = target_id
857
def add_entry(self, entry):
858
"""Add an entry to the list of entries"""
859
if self.entries.has_key(entry.id):
860
raise IDPresent(entry.id)
861
self.entries[entry.id] = entry
863
def my_sort(sequence, key, reverse=False):
864
"""A sort function that supports supplying a key for comparison
866
:param sequence: The sequence to sort
867
:param key: A callable object that returns the values to be compared
868
:param reverse: If true, sort in reverse order
871
def cmp_by_key(entry_a, entry_b):
876
return cmp(key(entry_a), key(entry_b))
877
sequence.sort(cmp_by_key)
879
def get_rename_entries(changeset, inventory, reverse):
880
"""Return a list of entries that will be renamed. Entries are sorted from
881
longest to shortest source path and from shortest to longest target path.
883
:param changeset: The changeset to look in
884
:type changeset: `Changeset`
885
:param inventory: The source of current tree paths for the given ids
886
:type inventory: Dictionary
887
:param reverse: If true, the changeset is being applied in reverse
889
:return: source entries and target entries as a tuple
892
source_entries = [x for x in changeset.entries.itervalues()
893
if x.needs_rename() or x.is_creation_or_deletion()]
894
# these are done from longest path to shortest, to avoid deleting a
895
# parent before its children are deleted/renamed
896
def longest_to_shortest(entry):
897
path = inventory.get(entry.id)
902
my_sort(source_entries, longest_to_shortest, reverse=True)
904
target_entries = source_entries[:]
905
# These are done from shortest to longest path, to avoid creating a
906
# child before its parent has been created/renamed
907
def shortest_to_longest(entry):
908
path = entry.get_new_path(inventory, changeset, reverse)
913
my_sort(target_entries, shortest_to_longest)
914
return (source_entries, target_entries)
916
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
917
conflict_handler, reverse):
918
"""Delete and rename entries as appropriate. Entries are renamed to temp
919
names. A map of id -> temp name (or None, for deletions) is returned.
921
:param source_entries: The entries to rename and delete
922
:type source_entries: List of `ChangesetEntry`
923
:param inventory: The map of id -> filename in the current tree
924
:type inventory: Dictionary
925
:param dir: The directory to apply changes to
927
:param reverse: Apply changes in reverse
929
:return: a mapping of id to temporary name
933
for i in range(len(source_entries)):
934
entry = source_entries[i]
935
if entry.is_deletion(reverse):
936
path = os.path.join(dir, inventory[entry.id])
937
entry.apply(path, conflict_handler, reverse)
938
temp_name[entry.id] = None
940
elif entry.needs_rename():
941
to_name = os.path.join(temp_dir, str(i))
942
src_path = inventory.get(entry.id)
943
if src_path is not None:
944
src_path = os.path.join(dir, src_path)
946
rename(src_path, to_name)
947
temp_name[entry.id] = to_name
949
if e.errno != errno.ENOENT:
951
if conflict_handler.missing_for_rename(src_path, to_name) \
958
def rename_to_new_create(changed_inventory, target_entries, inventory,
959
changeset, dir, conflict_handler, reverse):
960
"""Rename entries with temp names to their final names, create new files.
962
:param changed_inventory: A mapping of id to temporary name
963
:type changed_inventory: Dictionary
964
:param target_entries: The entries to apply changes to
965
:type target_entries: List of `ChangesetEntry`
966
:param changeset: The changeset to apply
967
:type changeset: `Changeset`
968
:param dir: The directory to apply changes to
970
:param reverse: If true, apply changes in reverse
973
for entry in target_entries:
974
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
975
if new_tree_path is None:
977
new_path = os.path.join(dir, new_tree_path)
978
old_path = changed_inventory.get(entry.id)
979
if bzrlib.osutils.lexists(new_path):
980
if conflict_handler.target_exists(entry, new_path, old_path) == \
983
if entry.is_creation(reverse):
984
entry.apply(new_path, conflict_handler, reverse)
985
changed_inventory[entry.id] = new_tree_path
986
elif entry.needs_rename():
990
rename(old_path, new_path)
991
changed_inventory[entry.id] = new_tree_path
993
raise Exception ("%s is missing" % new_path)
995
class TargetExists(Exception):
996
def __init__(self, entry, target):
997
msg = "The path %s already exists" % target
998
Exception.__init__(self, msg)
1000
self.target = target
1002
class RenameConflict(Exception):
1003
def __init__(self, id, this_name, base_name, other_name):
1004
msg = """Trees all have different names for a file
1008
id: %s""" % (this_name, base_name, other_name, id)
1009
Exception.__init__(self, msg)
1010
self.this_name = this_name
1011
self.base_name = base_name
1012
self_other_name = other_name
1014
class MoveConflict(Exception):
1015
def __init__(self, id, this_parent, base_parent, other_parent):
1016
msg = """The file is in different directories in every tree
1020
id: %s""" % (this_parent, base_parent, other_parent, id)
1021
Exception.__init__(self, msg)
1022
self.this_parent = this_parent
1023
self.base_parent = base_parent
1024
self_other_parent = other_parent
1026
class MergeConflict(Exception):
1027
def __init__(self, this_path):
1028
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1029
self.this_path = this_path
1031
class WrongOldContents(Exception):
1032
def __init__(self, filename):
1033
msg = "Contents mismatch deleting %s" % filename
1034
self.filename = filename
1035
Exception.__init__(self, msg)
1037
class WrongOldExecFlag(Exception):
1038
def __init__(self, filename, old_exec_flag, new_exec_flag):
1039
msg = "Executable flag missmatch on %s:\n" \
1040
"Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
1041
self.filename = filename
1042
Exception.__init__(self, msg)
1044
class RemoveContentsConflict(Exception):
1045
def __init__(self, filename):
1046
msg = "Conflict deleting %s, which has different contents in BASE"\
1047
" and THIS" % filename
1048
self.filename = filename
1049
Exception.__init__(self, msg)
1051
class DeletingNonEmptyDirectory(Exception):
1052
def __init__(self, filename):
1053
msg = "Trying to remove dir %s while it still had files" % filename
1054
self.filename = filename
1055
Exception.__init__(self, msg)
1058
class PatchTargetMissing(Exception):
1059
def __init__(self, filename):
1060
msg = "Attempt to patch %s, which does not exist" % filename
1061
Exception.__init__(self, msg)
1062
self.filename = filename
1064
class MissingForSetExec(Exception):
1065
def __init__(self, filename):
1066
msg = "Attempt to change permissions on %s, which does not exist" %\
1068
Exception.__init__(self, msg)
1069
self.filename = filename
1071
class MissingForRm(Exception):
1072
def __init__(self, filename):
1073
msg = "Attempt to remove missing path %s" % filename
1074
Exception.__init__(self, msg)
1075
self.filename = filename
1078
class MissingForRename(Exception):
1079
def __init__(self, filename, to_path):
1080
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1081
Exception.__init__(self, msg)
1082
self.filename = filename
1084
class NewContentsConflict(Exception):
1085
def __init__(self, filename):
1086
msg = "Conflicting contents for new file %s" % (filename)
1087
Exception.__init__(self, msg)
1089
class WeaveMergeConflict(Exception):
1090
def __init__(self, filename):
1091
msg = "Conflicting contents for file %s" % (filename)
1092
Exception.__init__(self, msg)
1094
class ThreewayContentsConflict(Exception):
1095
def __init__(self, filename):
1096
msg = "Conflicting contents for file %s" % (filename)
1097
Exception.__init__(self, msg)
1100
class MissingForMerge(Exception):
1101
def __init__(self, filename):
1102
msg = "The file %s was modified, but does not exist in this tree"\
1104
Exception.__init__(self, msg)
1107
class ExceptionConflictHandler(object):
1108
"""Default handler for merge exceptions.
1110
This throws an error on any kind of conflict. Conflict handlers can
1111
descend from this class if they have a better way to handle some or
1112
all types of conflict.
1114
def missing_parent(self, pathname):
1115
parent = os.path.dirname(pathname)
1116
raise Exception("Parent directory missing for %s" % pathname)
1118
def dir_exists(self, pathname):
1119
raise Exception("Directory already exists for %s" % pathname)
1121
def failed_hunks(self, pathname):
1122
raise Exception("Failed to apply some hunks for %s" % pathname)
1124
def target_exists(self, entry, target, old_path):
1125
raise TargetExists(entry, target)
1127
def rename_conflict(self, id, this_name, base_name, other_name):
1128
raise RenameConflict(id, this_name, base_name, other_name)
1130
def move_conflict(self, id, this_dir, base_dir, other_dir):
1131
raise MoveConflict(id, this_dir, base_dir, other_dir)
1133
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1135
raise MergeConflict(this_path)
1137
def wrong_old_contents(self, filename, expected_contents):
1138
raise WrongOldContents(filename)
1140
def rem_contents_conflict(self, filename, this_contents, base_contents):
1141
raise RemoveContentsConflict(filename)
1143
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1144
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1146
def rmdir_non_empty(self, filename):
1147
raise DeletingNonEmptyDirectory(filename)
1149
def link_name_exists(self, filename):
1150
raise TargetExists(filename)
1152
def patch_target_missing(self, filename, contents):
1153
raise PatchTargetMissing(filename)
1155
def missing_for_exec_flag(self, filename):
1156
raise MissingForExecFlag(filename)
1158
def missing_for_rm(self, filename, change):
1159
raise MissingForRm(filename)
1161
def missing_for_rename(self, filename, to_path):
1162
raise MissingForRename(filename, to_path)
1164
def missing_for_merge(self, file_id, other_path):
1165
raise MissingForMerge(other_path)
1167
def new_contents_conflict(self, filename, other_contents):
1168
raise NewContentsConflict(filename)
1170
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1171
raise WeaveMergeConflict(filename)
1173
def threeway_contents_conflict(self, filename, this_contents,
1174
base_contents, other_contents):
1175
raise ThreewayContentsConflict(filename)
1180
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1182
"""Apply a changeset to a directory.
1184
:param changeset: The changes to perform
1185
:type changeset: `Changeset`
1186
:param inventory: The mapping of id to filename for the directory
1187
:type inventory: Dictionary
1188
:param dir: The path of the directory to apply the changes to
1190
:param reverse: If true, apply the changes in reverse
1192
:return: The mapping of the changed entries
1195
if conflict_handler is None:
1196
conflict_handler = ExceptionConflictHandler()
1197
temp_dir = os.path.join(dir, "bzr-tree-change")
63
bytef, transport = do_catching_redirections(get_bundle, transport,
65
except errors.TooManyRedirections:
66
raise errors.NotABundle(transport.clone(filename).base)
67
except (errors.ConnectionReset, errors.ConnectionError), e:
69
except (errors.TransportError, errors.PathError), e:
70
raise errors.NotABundle(str(e))
73
# Abstraction leakage, SFTPTransport.get('directory')
74
# doesn't always fail at get() time. Sometimes it fails
75
# during read. And that raises a generic IOError with
76
# just the string 'Failure'
77
# StubSFTPServer does fail during get() (because of prefetch)
78
# so it has an opportunity to translate the error.
79
raise errors.NotABundle(str(e))
83
return MergeDirective.from_lines(bytef), transport
84
except errors.NotAMergeDirective:
87
return _serializer.read_bundle(bytef), transport
1201
if e.errno == errno.EEXIST:
1205
if e.errno == errno.ENOTEMPTY:
1206
raise OldFailedTreeOp()
1211
#apply changes that don't affect filenames
1212
for entry in changeset.entries.itervalues():
1213
if not entry.is_creation_or_deletion() and not entry.is_boring():
1214
if entry.id not in inventory:
1215
warning("entry {%s} no longer present, can't be updated",
1218
path = os.path.join(dir, inventory[entry.id])
1219
entry.apply(path, conflict_handler, reverse)
1221
# Apply renames in stages, to minimize conflicts:
1222
# Only files whose name or parent change are interesting, because their
1223
# target name may exist in the source tree. If a directory's name changes,
1224
# that doesn't make its children interesting.
1225
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1228
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1229
temp_dir, conflict_handler,
1232
rename_to_new_create(changed_inventory, target_entries, inventory,
1233
changeset, dir, conflict_handler, reverse)
1235
return changed_inventory
1238
def apply_changeset_tree(cset, tree, reverse=False):
1240
for entry in tree.source_inventory().itervalues():
1241
inventory[entry.id] = entry.path
1242
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1244
new_entries, remove_entries = \
1245
get_inventory_change(inventory, new_inventory, cset, reverse)
1246
tree.update_source_inventory(new_entries, remove_entries)
1249
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1252
for entry in cset.entries.itervalues():
1253
if entry.needs_rename():
1254
new_path = entry.get_new_path(inventory, cset)
1255
if new_path is None:
1256
remove_entries.append(entry.id)
1258
new_entries[new_path] = entry.id
1259
return new_entries, remove_entries
1262
def print_changeset(cset):
1263
"""Print all non-boring changeset entries
1265
:param cset: The changeset to print
1266
:type cset: `Changeset`
1268
for entry in cset.entries.itervalues():
1269
if entry.is_boring():
1272
print entry.summarize_name(cset)
1274
class CompositionFailure(Exception):
1275
def __init__(self, old_entry, new_entry, problem):
1276
msg = "Unable to conpose entries.\n %s" % problem
1277
Exception.__init__(self, msg)
1279
class IDMismatch(CompositionFailure):
1280
def __init__(self, old_entry, new_entry):
1281
problem = "Attempt to compose entries with different ids: %s and %s" %\
1282
(old_entry.id, new_entry.id)
1283
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1285
def compose_changesets(old_cset, new_cset):
1286
"""Combine two changesets into one. This works well for exact patching.
1287
Otherwise, not so well.
1289
:param old_cset: The first changeset that would be applied
1290
:type old_cset: `Changeset`
1291
:param new_cset: The second changeset that would be applied
1292
:type new_cset: `Changeset`
1293
:return: A changeset that combines the changes in both changesets
1296
composed = Changeset()
1297
for old_entry in old_cset.entries.itervalues():
1298
new_entry = new_cset.entries.get(old_entry.id)
1299
if new_entry is None:
1300
composed.add_entry(old_entry)
1302
composed_entry = compose_entries(old_entry, new_entry)
1303
if composed_entry.parent is not None or\
1304
composed_entry.new_parent is not None:
1305
composed.add_entry(composed_entry)
1306
for new_entry in new_cset.entries.itervalues():
1307
if not old_cset.entries.has_key(new_entry.id):
1308
composed.add_entry(new_entry)
1311
def compose_entries(old_entry, new_entry):
1312
"""Combine two entries into one.
1314
:param old_entry: The first entry that would be applied
1315
:type old_entry: ChangesetEntry
1316
:param old_entry: The second entry that would be applied
1317
:type old_entry: ChangesetEntry
1318
:return: A changeset entry combining both entries
1319
:rtype: `ChangesetEntry`
1321
if old_entry.id != new_entry.id:
1322
raise IDMismatch(old_entry, new_entry)
1323
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1325
if (old_entry.parent != old_entry.new_parent or
1326
new_entry.parent != new_entry.new_parent):
1327
output.new_parent = new_entry.new_parent
1329
if (old_entry.path != old_entry.new_path or
1330
new_entry.path != new_entry.new_path):
1331
output.new_path = new_entry.new_path
1333
output.contents_change = compose_contents(old_entry, new_entry)
1334
output.metadata_change = compose_metadata(old_entry, new_entry)
1337
def compose_contents(old_entry, new_entry):
1338
"""Combine the contents of two changeset entries. Entries are combined
1339
intelligently where possible, but the fallback behavior returns an
1342
:param old_entry: The first entry that would be applied
1343
:type old_entry: `ChangesetEntry`
1344
:param new_entry: The second entry that would be applied
1345
:type new_entry: `ChangesetEntry`
1346
:return: A combined contents change
1347
:rtype: anything supporting the apply(reverse=False) method
1349
old_contents = old_entry.contents_change
1350
new_contents = new_entry.contents_change
1351
if old_entry.contents_change is None:
1352
return new_entry.contents_change
1353
elif new_entry.contents_change is None:
1354
return old_entry.contents_change
1355
elif isinstance(old_contents, ReplaceContents) and \
1356
isinstance(new_contents, ReplaceContents):
1357
if old_contents.old_contents == new_contents.new_contents:
1360
return ReplaceContents(old_contents.old_contents,
1361
new_contents.new_contents)
1362
elif isinstance(old_contents, ApplySequence):
1363
output = ApplySequence(old_contents.changes)
1364
if isinstance(new_contents, ApplySequence):
1365
output.changes.extend(new_contents.changes)
1367
output.changes.append(new_contents)
1369
elif isinstance(new_contents, ApplySequence):
1370
output = ApplySequence((old_contents.changes,))
1371
output.extend(new_contents.changes)
1374
return ApplySequence((old_contents, new_contents))
1376
def compose_metadata(old_entry, new_entry):
1377
old_meta = old_entry.metadata_change
1378
new_meta = new_entry.metadata_change
1379
if old_meta is None:
1381
elif new_meta is None:
1383
elif (isinstance(old_meta, ChangeExecFlag) and
1384
isinstance(new_meta, ChangeExecFlag)):
1385
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1387
return ApplySequence(old_meta, new_meta)
1390
def changeset_is_null(changeset):
1391
for entry in changeset.entries.itervalues():
1392
if not entry.is_boring():
1396
class UnsupportedFiletype(Exception):
1397
def __init__(self, kind, full_path):
1398
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1400
Exception.__init__(self, msg)
1401
self.full_path = full_path
1404
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1405
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1408
class ChangesetGenerator(object):
1409
def __init__(self, tree_a, tree_b, interesting_ids=None):
1410
object.__init__(self)
1411
self.tree_a = tree_a
1412
self.tree_b = tree_b
1413
self._interesting_ids = interesting_ids
1415
def iter_both_tree_ids(self):
1416
for file_id in self.tree_a:
1418
for file_id in self.tree_b:
1419
if file_id not in self.tree_a:
1423
base_id = hasattr(self.tree_a, 'get_revision_id') and self.tree_a.get_revision_id()
1424
target_id = hasattr(self.tree_b, 'get_revision_id') and self.tree_b.get_revision_id()
1425
cset = Changeset(base_id, target_id)
1426
for file_id in self.iter_both_tree_ids():
1427
cs_entry = self.make_entry(file_id)
1428
if cs_entry is not None and not cs_entry.is_boring():
1429
cset.add_entry(cs_entry)
1431
for entry in list(cset.entries.itervalues()):
1432
if entry.parent != entry.new_parent:
1433
if not cset.entries.has_key(entry.parent) and\
1434
entry.parent != NULL_ID and entry.parent is not None:
1435
parent_entry = self.make_boring_entry(entry.parent)
1436
cset.add_entry(parent_entry)
1437
if not cset.entries.has_key(entry.new_parent) and\
1438
entry.new_parent != NULL_ID and \
1439
entry.new_parent is not None:
1440
parent_entry = self.make_boring_entry(entry.new_parent)
1441
cset.add_entry(parent_entry)
1444
def iter_inventory(self, tree):
1445
for file_id in tree:
1446
yield self.get_entry(file_id, tree)
1448
def get_entry(self, file_id, tree):
1449
if not tree.has_or_had_id(file_id):
1451
return tree.inventory[file_id]
1453
def get_entry_parent(self, entry):
1456
return entry.parent_id
1458
def get_path(self, file_id, tree):
1459
if not tree.has_or_had_id(file_id):
1461
path = tree.id2path(file_id)
1467
def make_basic_entry(self, file_id, only_interesting):
1468
entry_a = self.get_entry(file_id, self.tree_a)
1469
entry_b = self.get_entry(file_id, self.tree_b)
1470
if only_interesting and not self.is_interesting(entry_a, entry_b):
1472
parent = self.get_entry_parent(entry_a)
1473
path = self.get_path(file_id, self.tree_a)
1474
cs_entry = ChangesetEntry(file_id, parent, path)
1475
new_parent = self.get_entry_parent(entry_b)
1477
new_path = self.get_path(file_id, self.tree_b)
1479
cs_entry.new_path = new_path
1480
cs_entry.new_parent = new_parent
1483
def is_interesting(self, entry_a, entry_b):
1484
if self._interesting_ids is None:
1486
if entry_a is not None:
1487
file_id = entry_a.file_id
1488
elif entry_b is not None:
1489
file_id = entry_b.file_id
1492
return file_id in self._interesting_ids
1494
def make_boring_entry(self, id):
1495
cs_entry = self.make_basic_entry(id, only_interesting=False)
1496
if cs_entry.is_creation_or_deletion():
1497
return self.make_entry(id, only_interesting=False)
1502
def make_entry(self, id, only_interesting=True):
1503
cs_entry = self.make_basic_entry(id, only_interesting)
1505
if cs_entry is None:
1508
cs_entry.metadata_change = self.make_exec_flag_change(id)
1510
if id in self.tree_a and id in self.tree_b:
1511
a_sha1 = self.tree_a.get_file_sha1(id)
1512
b_sha1 = self.tree_b.get_file_sha1(id)
1513
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1516
cs_entry.contents_change = self.make_contents_change(id)
1519
def make_exec_flag_change(self, file_id):
1520
exec_flag_a = exec_flag_b = None
1521
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1522
exec_flag_a = self.tree_a.is_executable(file_id)
1524
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1525
exec_flag_b = self.tree_b.is_executable(file_id)
1527
if exec_flag_a == exec_flag_b:
1529
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1531
def make_contents_change(self, file_id):
1532
a_contents = get_contents(self.tree_a, file_id)
1533
b_contents = get_contents(self.tree_b, file_id)
1534
if a_contents == b_contents:
1536
return ReplaceContents(a_contents, b_contents)
1539
def get_contents(tree, file_id):
1540
"""Return the appropriate contents to create a copy of file_id from tree"""
1541
if file_id not in tree:
1543
kind = tree.kind(file_id)
1545
return TreeFileCreate(tree, file_id)
1546
elif kind in ("directory", "root_directory"):
1548
elif kind == "symlink":
1549
return SymlinkCreate(tree.get_symlink_target(file_id))
1551
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1554
def full_path(entry, tree):
1555
return os.path.join(tree.basedir, entry.path)
1557
def new_delete_entry(entry, tree, inventory, delete):
1558
if entry.path == "":
1561
parent = inventory[dirname(entry.path)].id
1562
cs_entry = ChangesetEntry(parent, entry.path)
1564
cs_entry.new_path = None
1565
cs_entry.new_parent = None
1567
cs_entry.path = None
1568
cs_entry.parent = None
1569
full_path = full_path(entry, tree)
1570
status = os.lstat(full_path)
1571
if stat.S_ISDIR(file_stat.st_mode):
1577
# XXX: Can't we unify this with the regular inventory object
1578
class Inventory(object):
1579
def __init__(self, inventory):
1580
self.inventory = inventory
1581
self.rinventory = None
1583
def get_rinventory(self):
1584
if self.rinventory is None:
1585
self.rinventory = invert_dict(self.inventory)
1586
return self.rinventory
1588
def get_path(self, id):
1589
return self.inventory.get(id)
1591
def get_name(self, id):
1592
path = self.get_path(id)
1596
return os.path.basename(path)
1598
def get_dir(self, id):
1599
path = self.get_path(id)
1604
return os.path.dirname(path)
1606
def get_parent(self, id):
1607
if self.get_path(id) is None:
1609
directory = self.get_dir(id)
1610
if directory == '.':
1612
if directory is None:
1614
return self.get_rinventory().get(directory)