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.symbol_versioning import deprecated_function, deprecated_in
20
from bzrlib.lazy_import import lazy_import
21
lazy_import(globals(), """
26
from bzrlib.bundle import serializer as _serializer
27
from bzrlib.merge_directive import MergeDirective
28
from bzrlib.transport import (
29
do_catching_redirections,
33
from bzrlib.trace import note
36
@deprecated_function(deprecated_in((1, 12, 0)))
37
def read_bundle_from_url(url):
38
return read_mergeable_from_url(url, _do_directive=False)
41
def read_mergeable_from_url(url, _do_directive=True, possible_transports=None):
42
"""Read mergable object from a given URL.
44
:return: An object supporting get_target_revision. Raises NotABundle if
45
the target is not a mergeable type.
47
child_transport = get_transport(url,
48
possible_transports=possible_transports)
49
transport = child_transport.clone('..')
50
filename = transport.relpath(child_transport.base)
51
mergeable, transport = read_mergeable_from_transport(transport, filename,
56
def read_mergeable_from_transport(transport, filename, _do_directive=True):
57
def get_bundle(transport):
58
return StringIO(transport.get_bytes(filename)), transport
60
def redirected_transport(transport, exception, redirection_notice):
61
note(redirection_notice)
62
url, filename = urlutils.split(exception.target,
63
exclude_trailing_slash=False)
65
raise errors.NotABundle('A directory cannot be a bundle')
66
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 shutil import rmtree
29
from itertools import izip
31
from bzrlib.trace import mutter, warning
32
from bzrlib.osutils import rename, sha_file, pathjoin, mkdtemp
34
from bzrlib.errors import BzrCheckError
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 = pathjoin(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
641
if self.path is None:
643
return os.path.dirname(self.path)
645
def __set_dir(self, dir):
646
self.path = pathjoin(dir, os.path.basename(self.path))
648
dir = property(__get_dir, __set_dir)
650
def __get_name(self):
651
if self.path is None:
653
return os.path.basename(self.path)
655
def __set_name(self, name):
656
self.path = pathjoin(os.path.dirname(self.path), name)
658
name = property(__get_name, __set_name)
660
def __get_new_dir(self):
661
if self.new_path is None:
663
return os.path.dirname(self.new_path)
665
def __set_new_dir(self, dir):
666
self.new_path = pathjoin(dir, os.path.basename(self.new_path))
668
new_dir = property(__get_new_dir, __set_new_dir)
670
def __get_new_name(self):
671
if self.new_path is None:
673
return os.path.basename(self.new_path)
675
def __set_new_name(self, name):
676
self.new_path = pathjoin(os.path.dirname(self.new_path), name)
678
new_name = property(__get_new_name, __set_new_name)
680
def needs_rename(self):
681
"""Determines whether the entry requires renaming.
686
return (self.parent != self.new_parent or self.name != self.new_name)
688
def is_deletion(self, reverse):
689
"""Return true if applying the entry would delete a file/directory.
691
:param reverse: if true, the changeset is being applied in reverse
694
return self.is_creation(not reverse)
696
def is_creation(self, reverse):
697
"""Return true if applying the entry would create a file/directory.
699
:param reverse: if true, the changeset is being applied in reverse
702
if self.contents_change is None:
705
return self.contents_change.is_deletion()
707
return self.contents_change.is_creation()
709
def is_creation_or_deletion(self):
710
"""Return true if applying the entry would create or delete a
715
return self.is_creation(False) or self.is_deletion(False)
717
def get_cset_path(self, mod=False):
718
"""Determine the path of the entry according to the changeset.
720
:param changeset: The changeset to derive the path from
721
:type changeset: `Changeset`
722
:param mod: If true, generate the MOD path. Otherwise, generate the \
724
:return: the path of the entry, or None if it did not exist in the \
726
:rtype: str or NoneType
729
if self.new_parent == NULL_ID:
731
elif self.new_parent is None:
735
if self.parent == NULL_ID:
737
elif self.parent is None:
741
def summarize_name(self, reverse=False):
742
"""Produce a one-line summary of the filename. Indicates renames as
743
old => new, indicates creation as None => new, indicates deletion as
746
:param changeset: The changeset to get paths from
747
:type changeset: `Changeset`
748
:param reverse: If true, reverse the names in the output
752
orig_path = self.get_cset_path(False)
753
mod_path = self.get_cset_path(True)
754
if orig_path and orig_path.startswith('./'):
755
orig_path = orig_path[2:]
756
if mod_path and mod_path.startswith('./'):
757
mod_path = mod_path[2:]
758
if orig_path == mod_path:
762
return "%s => %s" % (orig_path, mod_path)
764
return "%s => %s" % (mod_path, orig_path)
767
def get_new_path(self, id_map, changeset, reverse=False):
768
"""Determine the full pathname to rename to
770
:param id_map: The map of ids to filenames for the tree
771
:type id_map: Dictionary
772
:param changeset: The changeset to get data from
773
:type changeset: `Changeset`
774
:param reverse: If true, we're applying the changeset in reverse
778
mutter("Finding new path for %s", self.summarize_name())
782
from_dir = self.new_dir
784
from_name = self.new_name
786
parent = self.new_parent
787
to_dir = self.new_dir
789
to_name = self.new_name
790
from_name = self.name
795
if parent == NULL_ID or parent is None:
797
raise SourceRootHasName(self, to_name)
800
parent_entry = changeset.entries.get(parent)
801
if parent_entry is None:
802
dir = os.path.dirname(id_map[self.id])
804
mutter("path, new_path: %r %r", self.path, self.new_path)
805
dir = parent_entry.get_new_path(id_map, changeset, reverse)
806
if from_name == to_name:
807
name = os.path.basename(id_map[self.id])
810
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
811
return pathjoin(dir, name)
814
"""Determines whether the entry does nothing
816
:return: True if the entry does no renames or content changes
819
if self.contents_change is not None:
821
elif self.metadata_change is not None:
823
elif self.parent != self.new_parent:
825
elif self.name != self.new_name:
830
def apply(self, filename, conflict_handler, reverse=False):
831
"""Applies the file content and/or metadata changes.
833
:param filename: the filename of the entry
835
:param reverse: If true, apply the changes in reverse
838
if self.is_deletion(reverse) and self.metadata_change is not None:
839
self.metadata_change.apply(filename, conflict_handler, reverse)
840
if self.contents_change is not None:
841
self.contents_change.apply(filename, conflict_handler, reverse)
842
if not self.is_deletion(reverse) and self.metadata_change is not None:
843
self.metadata_change.apply(filename, conflict_handler, reverse)
845
class IDPresent(Exception):
846
def __init__(self, id):
847
msg = "Cannot add entry because that id has already been used:\n%s" %\
849
Exception.__init__(self, msg)
852
class Changeset(object):
853
"""A set of changes to apply"""
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 = pathjoin(dir, inventory[entry.id])
937
entry.apply(path, conflict_handler, reverse)
938
temp_name[entry.id] = None
940
elif entry.needs_rename():
941
if entry.is_creation(reverse):
943
to_name = pathjoin(temp_dir, str(i))
944
src_path = inventory.get(entry.id)
945
if src_path is not None:
946
src_path = pathjoin(dir, src_path)
948
rename(src_path, to_name)
949
temp_name[entry.id] = to_name
951
if e.errno != errno.ENOENT:
953
if conflict_handler.missing_for_rename(src_path, to_name) \
960
def rename_to_new_create(changed_inventory, target_entries, inventory,
961
changeset, dir, conflict_handler, reverse):
962
"""Rename entries with temp names to their final names, create new files.
964
:param changed_inventory: A mapping of id to temporary name
965
:type changed_inventory: Dictionary
966
:param target_entries: The entries to apply changes to
967
:type target_entries: List of `ChangesetEntry`
968
:param changeset: The changeset to apply
969
:type changeset: `Changeset`
970
:param dir: The directory to apply changes to
972
:param reverse: If true, apply changes in reverse
975
for entry in target_entries:
976
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
977
if new_tree_path is None:
979
new_path = pathjoin(dir, new_tree_path)
980
old_path = changed_inventory.get(entry.id)
981
if bzrlib.osutils.lexists(new_path):
982
if conflict_handler.target_exists(entry, new_path, old_path) == \
985
if entry.is_creation(reverse):
986
entry.apply(new_path, conflict_handler, reverse)
987
changed_inventory[entry.id] = new_tree_path
988
elif entry.needs_rename():
989
if entry.is_deletion(reverse):
994
mutter('rename %s to final name %s', old_path, new_path)
995
rename(old_path, new_path)
996
changed_inventory[entry.id] = new_tree_path
998
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
999
% (old_path, new_path, entry, e))
1001
class TargetExists(Exception):
1002
def __init__(self, entry, target):
1003
msg = "The path %s already exists" % target
1004
Exception.__init__(self, msg)
1006
self.target = target
1008
class RenameConflict(Exception):
1009
def __init__(self, id, this_name, base_name, other_name):
1010
msg = """Trees all have different names for a file
1014
id: %s""" % (this_name, base_name, other_name, id)
1015
Exception.__init__(self, msg)
1016
self.this_name = this_name
1017
self.base_name = base_name
1018
self_other_name = other_name
1020
class MoveConflict(Exception):
1021
def __init__(self, id, this_parent, base_parent, other_parent):
1022
msg = """The file is in different directories in every tree
1026
id: %s""" % (this_parent, base_parent, other_parent, id)
1027
Exception.__init__(self, msg)
1028
self.this_parent = this_parent
1029
self.base_parent = base_parent
1030
self_other_parent = other_parent
1032
class MergeConflict(Exception):
1033
def __init__(self, this_path):
1034
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1035
self.this_path = this_path
1037
class WrongOldContents(Exception):
1038
def __init__(self, filename):
1039
msg = "Contents mismatch deleting %s" % filename
1040
self.filename = filename
1041
Exception.__init__(self, msg)
1043
class WrongOldExecFlag(Exception):
1044
def __init__(self, filename, old_exec_flag, new_exec_flag):
1045
msg = "Executable flag missmatch on %s:\n" \
1046
"Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
1047
self.filename = filename
1048
Exception.__init__(self, msg)
1050
class RemoveContentsConflict(Exception):
1051
def __init__(self, filename):
1052
msg = "Conflict deleting %s, which has different contents in BASE"\
1053
" and THIS" % filename
1054
self.filename = filename
1055
Exception.__init__(self, msg)
1057
class DeletingNonEmptyDirectory(Exception):
1058
def __init__(self, filename):
1059
msg = "Trying to remove dir %s while it still had files" % filename
1060
self.filename = filename
1061
Exception.__init__(self, msg)
1064
class PatchTargetMissing(Exception):
1065
def __init__(self, filename):
1066
msg = "Attempt to patch %s, which does not exist" % filename
1067
Exception.__init__(self, msg)
1068
self.filename = filename
1070
class MissingForSetExec(Exception):
1071
def __init__(self, filename):
1072
msg = "Attempt to change permissions on %s, which does not exist" %\
1074
Exception.__init__(self, msg)
1075
self.filename = filename
1077
class MissingForRm(Exception):
1078
def __init__(self, filename):
1079
msg = "Attempt to remove missing path %s" % filename
1080
Exception.__init__(self, msg)
1081
self.filename = filename
1084
class MissingForRename(Exception):
1085
def __init__(self, filename, to_path):
1086
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1087
Exception.__init__(self, msg)
1088
self.filename = filename
1090
class NewContentsConflict(Exception):
1091
def __init__(self, filename):
1092
msg = "Conflicting contents for new file %s" % (filename)
1093
Exception.__init__(self, msg)
1095
class WeaveMergeConflict(Exception):
1096
def __init__(self, filename):
1097
msg = "Conflicting contents for file %s" % (filename)
1098
Exception.__init__(self, msg)
1100
class ThreewayContentsConflict(Exception):
1101
def __init__(self, filename):
1102
msg = "Conflicting contents for file %s" % (filename)
1103
Exception.__init__(self, msg)
1106
class MissingForMerge(Exception):
1107
def __init__(self, filename):
1108
msg = "The file %s was modified, but does not exist in this tree"\
1110
Exception.__init__(self, msg)
1113
class ExceptionConflictHandler(object):
1114
"""Default handler for merge exceptions.
1116
This throws an error on any kind of conflict. Conflict handlers can
1117
descend from this class if they have a better way to handle some or
1118
all types of conflict.
1120
def missing_parent(self, pathname):
1121
parent = os.path.dirname(pathname)
1122
raise Exception("Parent directory missing for %s" % pathname)
1124
def dir_exists(self, pathname):
1125
raise Exception("Directory already exists for %s" % pathname)
1127
def failed_hunks(self, pathname):
1128
raise Exception("Failed to apply some hunks for %s" % pathname)
1130
def target_exists(self, entry, target, old_path):
1131
raise TargetExists(entry, target)
1133
def rename_conflict(self, id, this_name, base_name, other_name):
1134
raise RenameConflict(id, this_name, base_name, other_name)
1136
def move_conflict(self, id, this_dir, base_dir, other_dir):
1137
raise MoveConflict(id, this_dir, base_dir, other_dir)
1139
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1141
raise MergeConflict(this_path)
1143
def wrong_old_contents(self, filename, expected_contents):
1144
raise WrongOldContents(filename)
1146
def rem_contents_conflict(self, filename, this_contents, base_contents):
1147
raise RemoveContentsConflict(filename)
1149
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1150
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1152
def rmdir_non_empty(self, filename):
1153
raise DeletingNonEmptyDirectory(filename)
1155
def link_name_exists(self, filename):
1156
raise TargetExists(filename)
1158
def patch_target_missing(self, filename, contents):
1159
raise PatchTargetMissing(filename)
1161
def missing_for_exec_flag(self, filename):
1162
raise MissingForExecFlag(filename)
1164
def missing_for_rm(self, filename, change):
1165
raise MissingForRm(filename)
1167
def missing_for_rename(self, filename, to_path):
1168
raise MissingForRename(filename, to_path)
1170
def missing_for_merge(self, file_id, other_path):
1171
raise MissingForMerge(other_path)
1173
def new_contents_conflict(self, filename, other_contents):
1174
raise NewContentsConflict(filename)
1176
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1177
raise WeaveMergeConflict(filename)
1179
def threeway_contents_conflict(self, filename, this_contents,
1180
base_contents, other_contents):
1181
raise ThreewayContentsConflict(filename)
1186
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1188
"""Apply a changeset to a directory.
1190
:param changeset: The changes to perform
1191
:type changeset: `Changeset`
1192
:param inventory: The mapping of id to filename for the directory
1193
:type inventory: Dictionary
1194
:param dir: The path of the directory to apply the changes to
1196
:param reverse: If true, apply the changes in reverse
1198
:return: The mapping of the changed entries
1201
if conflict_handler is None:
1202
conflict_handler = ExceptionConflictHandler()
1203
temp_dir = pathjoin(dir, "bzr-tree-change")
69
bytef, transport = do_catching_redirections(get_bundle, transport,
71
except errors.TooManyRedirections:
72
raise errors.NotABundle(transport.clone(filename).base)
73
except (errors.ConnectionReset, errors.ConnectionError), e:
75
except (errors.TransportError, errors.PathError), e:
76
raise errors.NotABundle(str(e))
79
# Abstraction leakage, SFTPTransport.get('directory')
80
# doesn't always fail at get() time. Sometimes it fails
81
# during read. And that raises a generic IOError with
82
# just the string 'Failure'
83
# StubSFTPServer does fail during get() (because of prefetch)
84
# so it has an opportunity to translate the error.
85
raise errors.NotABundle(str(e))
89
return MergeDirective.from_lines(bytef), transport
90
except errors.NotAMergeDirective:
93
return _serializer.read_bundle(bytef), transport
1207
if e.errno == errno.EEXIST:
1211
if e.errno == errno.ENOTEMPTY:
1212
raise OldFailedTreeOp()
1217
#apply changes that don't affect filenames
1218
for entry in changeset.entries.itervalues():
1219
if not entry.is_creation_or_deletion() and not entry.is_boring():
1220
if entry.id not in inventory:
1221
warning("entry {%s} no longer present, can't be updated",
1224
path = pathjoin(dir, inventory[entry.id])
1225
entry.apply(path, conflict_handler, reverse)
1227
# Apply renames in stages, to minimize conflicts:
1228
# Only files whose name or parent change are interesting, because their
1229
# target name may exist in the source tree. If a directory's name changes,
1230
# that doesn't make its children interesting.
1231
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1234
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1235
temp_dir, conflict_handler,
1238
rename_to_new_create(changed_inventory, target_entries, inventory,
1239
changeset, dir, conflict_handler, reverse)
1241
return changed_inventory
1244
def apply_changeset_tree(cset, tree, reverse=False):
1246
for entry in tree.source_inventory().itervalues():
1247
inventory[entry.id] = entry.path
1248
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1250
new_entries, remove_entries = \
1251
get_inventory_change(inventory, new_inventory, cset, reverse)
1252
tree.update_source_inventory(new_entries, remove_entries)
1255
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1258
for entry in cset.entries.itervalues():
1259
if entry.needs_rename():
1260
new_path = entry.get_new_path(inventory, cset)
1261
if new_path is None:
1262
remove_entries.append(entry.id)
1264
new_entries[new_path] = entry.id
1265
return new_entries, remove_entries
1268
def print_changeset(cset):
1269
"""Print all non-boring changeset entries
1271
:param cset: The changeset to print
1272
:type cset: `Changeset`
1274
for entry in cset.entries.itervalues():
1275
if entry.is_boring():
1278
print entry.summarize_name(cset)
1280
class CompositionFailure(Exception):
1281
def __init__(self, old_entry, new_entry, problem):
1282
msg = "Unable to conpose entries.\n %s" % problem
1283
Exception.__init__(self, msg)
1285
class IDMismatch(CompositionFailure):
1286
def __init__(self, old_entry, new_entry):
1287
problem = "Attempt to compose entries with different ids: %s and %s" %\
1288
(old_entry.id, new_entry.id)
1289
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1291
def compose_changesets(old_cset, new_cset):
1292
"""Combine two changesets into one. This works well for exact patching.
1293
Otherwise, not so well.
1295
:param old_cset: The first changeset that would be applied
1296
:type old_cset: `Changeset`
1297
:param new_cset: The second changeset that would be applied
1298
:type new_cset: `Changeset`
1299
:return: A changeset that combines the changes in both changesets
1302
composed = Changeset()
1303
for old_entry in old_cset.entries.itervalues():
1304
new_entry = new_cset.entries.get(old_entry.id)
1305
if new_entry is None:
1306
composed.add_entry(old_entry)
1308
composed_entry = compose_entries(old_entry, new_entry)
1309
if composed_entry.parent is not None or\
1310
composed_entry.new_parent is not None:
1311
composed.add_entry(composed_entry)
1312
for new_entry in new_cset.entries.itervalues():
1313
if not old_cset.entries.has_key(new_entry.id):
1314
composed.add_entry(new_entry)
1317
def compose_entries(old_entry, new_entry):
1318
"""Combine two entries into one.
1320
:param old_entry: The first entry that would be applied
1321
:type old_entry: ChangesetEntry
1322
:param old_entry: The second entry that would be applied
1323
:type old_entry: ChangesetEntry
1324
:return: A changeset entry combining both entries
1325
:rtype: `ChangesetEntry`
1327
if old_entry.id != new_entry.id:
1328
raise IDMismatch(old_entry, new_entry)
1329
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1331
if (old_entry.parent != old_entry.new_parent or
1332
new_entry.parent != new_entry.new_parent):
1333
output.new_parent = new_entry.new_parent
1335
if (old_entry.path != old_entry.new_path or
1336
new_entry.path != new_entry.new_path):
1337
output.new_path = new_entry.new_path
1339
output.contents_change = compose_contents(old_entry, new_entry)
1340
output.metadata_change = compose_metadata(old_entry, new_entry)
1343
def compose_contents(old_entry, new_entry):
1344
"""Combine the contents of two changeset entries. Entries are combined
1345
intelligently where possible, but the fallback behavior returns an
1348
:param old_entry: The first entry that would be applied
1349
:type old_entry: `ChangesetEntry`
1350
:param new_entry: The second entry that would be applied
1351
:type new_entry: `ChangesetEntry`
1352
:return: A combined contents change
1353
:rtype: anything supporting the apply(reverse=False) method
1355
old_contents = old_entry.contents_change
1356
new_contents = new_entry.contents_change
1357
if old_entry.contents_change is None:
1358
return new_entry.contents_change
1359
elif new_entry.contents_change is None:
1360
return old_entry.contents_change
1361
elif isinstance(old_contents, ReplaceContents) and \
1362
isinstance(new_contents, ReplaceContents):
1363
if old_contents.old_contents == new_contents.new_contents:
1366
return ReplaceContents(old_contents.old_contents,
1367
new_contents.new_contents)
1368
elif isinstance(old_contents, ApplySequence):
1369
output = ApplySequence(old_contents.changes)
1370
if isinstance(new_contents, ApplySequence):
1371
output.changes.extend(new_contents.changes)
1373
output.changes.append(new_contents)
1375
elif isinstance(new_contents, ApplySequence):
1376
output = ApplySequence((old_contents.changes,))
1377
output.extend(new_contents.changes)
1380
return ApplySequence((old_contents, new_contents))
1382
def compose_metadata(old_entry, new_entry):
1383
old_meta = old_entry.metadata_change
1384
new_meta = new_entry.metadata_change
1385
if old_meta is None:
1387
elif new_meta is None:
1389
elif (isinstance(old_meta, ChangeExecFlag) and
1390
isinstance(new_meta, ChangeExecFlag)):
1391
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1393
return ApplySequence(old_meta, new_meta)
1396
def changeset_is_null(changeset):
1397
for entry in changeset.entries.itervalues():
1398
if not entry.is_boring():
1402
class UnsupportedFiletype(Exception):
1403
def __init__(self, kind, full_path):
1404
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1406
Exception.__init__(self, msg)
1407
self.full_path = full_path
1410
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1411
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1414
class ChangesetGenerator(object):
1415
def __init__(self, tree_a, tree_b, interesting_ids=None):
1416
object.__init__(self)
1417
self.tree_a = tree_a
1418
self.tree_b = tree_b
1419
self._interesting_ids = interesting_ids
1421
def iter_both_tree_ids(self):
1422
for file_id in self.tree_a:
1424
for file_id in self.tree_b:
1425
if file_id not in self.tree_a:
1430
for file_id in self.iter_both_tree_ids():
1431
cs_entry = self.make_entry(file_id)
1432
if cs_entry is not None and not cs_entry.is_boring():
1433
cset.add_entry(cs_entry)
1435
for entry in list(cset.entries.itervalues()):
1436
if entry.parent != entry.new_parent:
1437
if not cset.entries.has_key(entry.parent) and\
1438
entry.parent != NULL_ID and entry.parent is not None:
1439
parent_entry = self.make_boring_entry(entry.parent)
1440
cset.add_entry(parent_entry)
1441
if not cset.entries.has_key(entry.new_parent) and\
1442
entry.new_parent != NULL_ID and \
1443
entry.new_parent is not None:
1444
parent_entry = self.make_boring_entry(entry.new_parent)
1445
cset.add_entry(parent_entry)
1448
def iter_inventory(self, tree):
1449
for file_id in tree:
1450
yield self.get_entry(file_id, tree)
1452
def get_entry(self, file_id, tree):
1453
if not tree.has_or_had_id(file_id):
1455
return tree.inventory[file_id]
1457
def get_entry_parent(self, entry):
1460
return entry.parent_id
1462
def get_path(self, file_id, tree):
1463
if not tree.has_or_had_id(file_id):
1465
path = tree.id2path(file_id)
1471
def make_basic_entry(self, file_id, only_interesting):
1472
entry_a = self.get_entry(file_id, self.tree_a)
1473
entry_b = self.get_entry(file_id, self.tree_b)
1474
if only_interesting and not self.is_interesting(entry_a, entry_b):
1476
parent = self.get_entry_parent(entry_a)
1477
path = self.get_path(file_id, self.tree_a)
1478
cs_entry = ChangesetEntry(file_id, parent, path)
1479
new_parent = self.get_entry_parent(entry_b)
1481
new_path = self.get_path(file_id, self.tree_b)
1483
cs_entry.new_path = new_path
1484
cs_entry.new_parent = new_parent
1487
def is_interesting(self, entry_a, entry_b):
1488
if self._interesting_ids is None:
1490
if entry_a is not None:
1491
file_id = entry_a.file_id
1492
elif entry_b is not None:
1493
file_id = entry_b.file_id
1496
return file_id in self._interesting_ids
1498
def make_boring_entry(self, id):
1499
cs_entry = self.make_basic_entry(id, only_interesting=False)
1500
if cs_entry.is_creation_or_deletion():
1501
return self.make_entry(id, only_interesting=False)
1506
def make_entry(self, id, only_interesting=True):
1507
cs_entry = self.make_basic_entry(id, only_interesting)
1509
if cs_entry is None:
1512
cs_entry.metadata_change = self.make_exec_flag_change(id)
1514
if id in self.tree_a and id in self.tree_b:
1515
a_sha1 = self.tree_a.get_file_sha1(id)
1516
b_sha1 = self.tree_b.get_file_sha1(id)
1517
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1520
cs_entry.contents_change = self.make_contents_change(id)
1523
def make_exec_flag_change(self, file_id):
1524
exec_flag_a = exec_flag_b = None
1525
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1526
exec_flag_a = self.tree_a.is_executable(file_id)
1528
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1529
exec_flag_b = self.tree_b.is_executable(file_id)
1531
if exec_flag_a == exec_flag_b:
1533
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1535
def make_contents_change(self, file_id):
1536
a_contents = get_contents(self.tree_a, file_id)
1537
b_contents = get_contents(self.tree_b, file_id)
1538
if a_contents == b_contents:
1540
return ReplaceContents(a_contents, b_contents)
1543
def get_contents(tree, file_id):
1544
"""Return the appropriate contents to create a copy of file_id from tree"""
1545
if file_id not in tree:
1547
kind = tree.kind(file_id)
1549
return TreeFileCreate(tree, file_id)
1550
elif kind in ("directory", "root_directory"):
1552
elif kind == "symlink":
1553
return SymlinkCreate(tree.get_symlink_target(file_id))
1555
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1558
def full_path(entry, tree):
1559
return pathjoin(tree.basedir, entry.path)
1561
def new_delete_entry(entry, tree, inventory, delete):
1562
if entry.path == "":
1565
parent = inventory[dirname(entry.path)].id
1566
cs_entry = ChangesetEntry(parent, entry.path)
1568
cs_entry.new_path = None
1569
cs_entry.new_parent = None
1571
cs_entry.path = None
1572
cs_entry.parent = None
1573
full_path = full_path(entry, tree)
1574
status = os.lstat(full_path)
1575
if stat.S_ISDIR(file_stat.st_mode):
1581
# XXX: Can't we unify this with the regular inventory object
1582
class Inventory(object):
1583
def __init__(self, inventory):
1584
self.inventory = inventory
1585
self.rinventory = None
1587
def get_rinventory(self):
1588
if self.rinventory is None:
1589
self.rinventory = invert_dict(self.inventory)
1590
return self.rinventory
1592
def get_path(self, id):
1593
return self.inventory.get(id)
1595
def get_name(self, id):
1596
path = self.get_path(id)
1600
return os.path.basename(path)
1602
def get_dir(self, id):
1603
path = self.get_path(id)
1608
return os.path.dirname(path)
1610
def get_parent(self, id):
1611
if self.get_path(id) is None:
1613
directory = self.get_dir(id)
1614
if directory == '.':
1616
if directory is None:
1618
return self.get_rinventory().get(directory)