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