1
# Copyright (C) 2006 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from tempfile import mkdtemp
20
from shutil import rmtree
21
from stat import S_ISREG
23
from bzrlib import BZRDIR
24
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
25
ReusingTransform, NotVersionedError, CantMoveRoot,
26
WorkingTreeNotRevision)
27
from bzrlib.inventory import InventoryEntry
28
from bzrlib.osutils import file_kind, supports_executable, pathjoin
29
from bzrlib.merge3 import Merge3
30
from bzrlib.trace import mutter
32
ROOT_PARENT = "root-parent"
34
def unique_add(map, key, value):
36
raise DuplicateKey(key=key)
39
class TreeTransform(object):
40
"""Represent a tree transformation."""
41
def __init__(self, tree):
42
"""Note: a write lock is taken on the tree.
44
Use TreeTransform.finalize() to release the lock
48
self._tree.lock_write()
52
self._new_contents = {}
53
self._removed_contents = set()
54
self._new_executability = {}
57
self._removed_id = set()
58
self._tree_path_ids = {}
59
self._tree_id_paths = {}
60
self._new_root = self.get_id_tree(tree.get_root_id())
62
# XXX use the WorkingTree LockableFiles, when available
63
control_files = self._tree.branch.control_files
64
self._limbodir = control_files.controlfilename('limbo')
65
os.mkdir(self._limbodir)
68
"""Release the working tree lock, if held."""
69
if self._tree is None:
71
for trans_id, kind in self._new_contents.iteritems():
72
path = self._limbo_name(trans_id)
73
if kind == "directory":
77
os.rmdir(self._limbodir)
82
"""Produce a new tranform id"""
83
new_id = "new-%s" % self._id_number
87
def create_path(self, name, parent):
88
"""Assign a transaction id to a new path"""
89
trans_id = self._assign_id()
90
unique_add(self._new_name, trans_id, name)
91
unique_add(self._new_parent, trans_id, parent)
94
def adjust_path(self, name, parent, trans_id):
95
"""Change the path that is assigned to a transaction id."""
96
if trans_id == self._new_root:
98
self._new_name[trans_id] = name
99
self._new_parent[trans_id] = parent
101
def adjust_root_path(self, name, parent):
102
"""Emulate moving the root by moving all children, instead.
104
We do this by undoing the association of root's transaction id with the
105
current tree. This allows us to create a new directory with that
106
transaction id. We unversion the root directory and version the
107
physically new directory, and hope someone versions the tree root
110
old_root = self._new_root
111
old_root_file_id = self.final_file_id(old_root)
112
# force moving all children of root
113
for child_id in self.iter_tree_children(old_root):
114
if child_id != parent:
115
self.adjust_path(self.final_name(child_id),
116
self.final_parent(child_id), child_id)
117
file_id = self.final_file_id(child_id)
118
if file_id is not None:
119
self.unversion_file(child_id)
120
self.version_file(file_id, child_id)
122
# the physical root needs a new transaction id
123
self._tree_path_ids.pop("")
124
self._tree_id_paths.pop(old_root)
125
self._new_root = self.get_id_tree(self._tree.get_root_id())
126
if parent == old_root:
127
parent = self._new_root
128
self.adjust_path(name, parent, old_root)
129
self.create_directory(old_root)
130
self.version_file(old_root_file_id, old_root)
131
self.unversion_file(self._new_root)
133
def get_id_tree(self, inventory_id):
134
"""Determine the transaction id of a working tree file.
136
This reflects only files that already exist, not ones that will be
137
added by transactions.
139
return self.get_tree_path_id(self._tree.id2path(inventory_id))
141
def get_trans_id(self, file_id):
143
Determine or set the transaction id associated with a file ID.
144
A new id is only created for file_ids that were never present. If
145
a transaction has been unversioned, it is deliberately still returned.
146
(this will likely lead to an unversioned parent conflict.)
148
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
149
return self._r_new_id[file_id]
150
elif file_id in self._tree.inventory:
151
return self.get_id_tree(file_id)
153
trans_id = self._assign_id()
154
self.version_file(file_id, trans_id)
157
def canonical_path(self, path):
158
"""Get the canonical tree-relative path"""
159
# don't follow final symlinks
160
dirname, basename = os.path.split(self._tree.abspath(path))
161
dirname = os.path.realpath(dirname)
162
return self._tree.relpath(os.path.join(dirname, basename))
164
def get_tree_path_id(self, path):
165
"""Determine (and maybe set) the transaction ID for a tree path."""
166
path = self.canonical_path(path)
167
if path not in self._tree_path_ids:
168
self._tree_path_ids[path] = self._assign_id()
169
self._tree_id_paths[self._tree_path_ids[path]] = path
170
return self._tree_path_ids[path]
172
def get_tree_parent(self, trans_id):
173
"""Determine id of the parent in the tree."""
174
path = self._tree_id_paths[trans_id]
177
return self.get_tree_path_id(os.path.dirname(path))
179
def create_file(self, contents, trans_id, mode_id=None):
180
"""Schedule creation of a new file.
184
Contents is an iterator of strings, all of which will be written
185
to the target destination.
187
New file takes the permissions of any existing file with that id,
188
unless mode_id is specified.
190
f = file(self._limbo_name(trans_id), 'wb')
191
unique_add(self._new_contents, trans_id, 'file')
192
for segment in contents:
195
self._set_mode(trans_id, mode_id, S_ISREG)
197
def _set_mode(self, trans_id, mode_id, typefunc):
201
old_path = self._tree_id_paths[mode_id]
205
mode = os.stat(old_path).st_mode
207
if e.errno == errno.ENOENT:
212
os.chmod(self._limbo_name(trans_id), mode)
214
def create_directory(self, trans_id):
215
"""Schedule creation of a new directory.
217
See also new_directory.
219
os.mkdir(self._limbo_name(trans_id))
220
unique_add(self._new_contents, trans_id, 'directory')
222
def create_symlink(self, target, trans_id):
223
"""Schedule creation of a new symbolic link.
225
target is a bytestring.
226
See also new_symlink.
228
os.symlink(target, self._limbo_name(trans_id))
229
unique_add(self._new_contents, trans_id, 'symlink')
232
def delete_any(full_path):
236
# We may be renaming a dangling inventory id
237
if e.errno != errno.EISDIR and e.errno != errno.EACCES:
241
def cancel_creation(self, trans_id):
242
del self._new_contents[trans_id]
243
self.delete_any(self._limbo_name(trans_id))
245
def delete_contents(self, trans_id):
246
"""Schedule the contents of a path entry for deletion"""
247
self.tree_kind(trans_id)
248
self._removed_contents.add(trans_id)
250
def cancel_deletion(self, trans_id):
251
"""Cancel a scheduled deletion"""
252
self._removed_contents.remove(trans_id)
254
def unversion_file(self, trans_id):
255
"""Schedule a path entry to become unversioned"""
256
self._removed_id.add(trans_id)
258
def delete_versioned(self, trans_id):
259
"""Delete and unversion a versioned file"""
260
self.delete_contents(trans_id)
261
self.unversion_file(trans_id)
263
def set_executability(self, executability, trans_id):
264
"""Schedule setting of the 'execute' bit"""
265
if executability is None:
266
del self._new_executability[trans_id]
268
unique_add(self._new_executability, trans_id, executability)
270
def version_file(self, file_id, trans_id):
271
"""Schedule a file to become versioned."""
272
unique_add(self._new_id, trans_id, file_id)
273
unique_add(self._r_new_id, file_id, trans_id)
275
def cancel_versioning(self, trans_id):
276
"""Undo a previous versioning of a file"""
277
file_id = self._new_id[trans_id]
278
del self._new_id[trans_id]
279
del self._r_new_id[file_id]
282
"""Determine the paths of all new and changed files"""
284
fp = FinalPaths(self._new_root, self)
285
for id_set in (self._new_name, self._new_parent, self._new_contents,
286
self._new_id, self._new_executability):
287
new_ids.update(id_set)
288
new_paths = [(fp.get_path(t), t) for t in new_ids]
292
def tree_kind(self, trans_id):
293
"""Determine the file kind in the working tree.
295
Raises NoSuchFile if the file does not exist
297
path = self._tree_id_paths.get(trans_id)
299
raise NoSuchFile(None)
301
return file_kind(self._tree.abspath(path))
303
if e.errno != errno.ENOENT:
306
raise NoSuchFile(path)
308
def final_kind(self, trans_id):
310
Determine the final file kind, after any changes applied.
312
Raises NoSuchFile if the file does not exist/has no contents.
313
(It is conceivable that a path would be created without the
314
corresponding contents insertion command)
316
if trans_id in self._new_contents:
317
return self._new_contents[trans_id]
318
elif trans_id in self._removed_contents:
319
raise NoSuchFile(None)
321
return self.tree_kind(trans_id)
323
def get_tree_file_id(self, trans_id):
324
"""Determine the file id associated with the trans_id in the tree"""
326
path = self._tree_id_paths[trans_id]
328
# the file is a new, unversioned file, or invalid trans_id
330
# the file is old; the old id is still valid
331
if self._new_root == trans_id:
332
return self._tree.inventory.root.file_id
333
return self._tree.path2id(path)
335
def final_file_id(self, trans_id):
337
Determine the file id after any changes are applied, or None.
339
None indicates that the file will not be versioned after changes are
343
# there is a new id for this file
344
return self._new_id[trans_id]
346
if trans_id in self._removed_id:
348
return self.get_tree_file_id(trans_id)
350
def final_parent(self, trans_id):
352
Determine the parent file_id, after any changes are applied.
354
ROOT_PARENT is returned for the tree root.
357
return self._new_parent[trans_id]
359
return self.get_tree_parent(trans_id)
361
def final_name(self, trans_id):
362
"""Determine the final filename, after all changes are applied."""
364
return self._new_name[trans_id]
366
return os.path.basename(self._tree_id_paths[trans_id])
368
def _by_parent(self):
369
"""Return a map of parent: children for known parents.
371
Only new paths and parents of tree files with assigned ids are used.
374
items = list(self._new_parent.iteritems())
375
items.extend((t, self.final_parent(t)) for t in
376
self._tree_id_paths.keys())
377
for trans_id, parent_id in items:
378
if parent_id not in by_parent:
379
by_parent[parent_id] = set()
380
by_parent[parent_id].add(trans_id)
383
def path_changed(self, trans_id):
384
return trans_id in self._new_name or trans_id in self._new_parent
386
def find_conflicts(self):
387
"""Find any violations of inventory or filesystem invariants"""
388
if self.__done is True:
389
raise ReusingTransform()
391
# ensure all children of all existent parents are known
392
# all children of non-existent parents are known, by definition.
393
self._add_tree_children()
394
by_parent = self._by_parent()
395
conflicts.extend(self._unversioned_parents(by_parent))
396
conflicts.extend(self._parent_loops())
397
conflicts.extend(self._duplicate_entries(by_parent))
398
conflicts.extend(self._duplicate_ids())
399
conflicts.extend(self._parent_type_conflicts(by_parent))
400
conflicts.extend(self._improper_versioning())
401
conflicts.extend(self._executability_conflicts())
404
def _add_tree_children(self):
406
Add all the children of all active parents to the known paths.
408
Active parents are those which gain children, and those which are
409
removed. This is a necessary first step in detecting conflicts.
411
parents = self._by_parent().keys()
412
parents.extend([t for t in self._removed_contents if
413
self.tree_kind(t) == 'directory'])
414
for trans_id in self._removed_id:
415
file_id = self.get_tree_file_id(trans_id)
416
if self._tree.inventory[file_id].kind in ('directory',
418
parents.append(trans_id)
420
for parent_id in parents:
421
# ensure that all children are registered with the transaction
422
list(self.iter_tree_children(parent_id))
424
def iter_tree_children(self, parent_id):
425
"""Iterate through the entry's tree children, if any"""
427
path = self._tree_id_paths[parent_id]
431
children = os.listdir(self._tree.abspath(path))
433
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
437
for child in children:
438
childpath = joinpath(path, child)
439
if childpath == BZRDIR:
441
yield self.get_tree_path_id(childpath)
443
def _parent_loops(self):
444
"""No entry should be its own ancestor"""
446
for trans_id in self._new_parent:
449
while parent_id is not ROOT_PARENT:
451
parent_id = self.final_parent(parent_id)
452
if parent_id == trans_id:
453
conflicts.append(('parent loop', trans_id))
454
if parent_id in seen:
458
def _unversioned_parents(self, by_parent):
459
"""If parent directories are versioned, children must be versioned."""
461
for parent_id, children in by_parent.iteritems():
462
if parent_id is ROOT_PARENT:
464
if self.final_file_id(parent_id) is not None:
466
for child_id in children:
467
if self.final_file_id(child_id) is not None:
468
conflicts.append(('unversioned parent', parent_id))
472
def _improper_versioning(self):
474
Cannot version a file with no contents, or a bad type.
476
However, existing entries with no contents are okay.
479
for trans_id in self._new_id.iterkeys():
481
kind = self.final_kind(trans_id)
483
conflicts.append(('versioning no contents', trans_id))
485
if not InventoryEntry.versionable_kind(kind):
486
conflicts.append(('versioning bad kind', trans_id, kind))
489
def _executability_conflicts(self):
490
"""Check for bad executability changes.
492
Only versioned files may have their executability set, because
493
1. only versioned entries can have executability under windows
494
2. only files can be executable. (The execute bit on a directory
495
does not indicate searchability)
498
for trans_id in self._new_executability:
499
if self.final_file_id(trans_id) is None:
500
conflicts.append(('unversioned executability', trans_id))
503
non_file = self.final_kind(trans_id) != "file"
507
conflicts.append(('non-file executability', trans_id))
510
def _duplicate_entries(self, by_parent):
511
"""No directory may have two entries with the same name."""
513
for children in by_parent.itervalues():
514
name_ids = [(self.final_name(t), t) for t in children]
518
for name, trans_id in name_ids:
519
if name == last_name:
520
conflicts.append(('duplicate', last_trans_id, trans_id,
523
last_trans_id = trans_id
526
def _duplicate_ids(self):
527
"""Each inventory id may only be used once"""
529
removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
531
active_tree_ids = set((f for f in self._tree.inventory if
532
f not in removed_tree_ids))
533
for trans_id, file_id in self._new_id.iteritems():
534
if file_id in active_tree_ids:
535
old_trans_id = self.get_id_tree(file_id)
536
conflicts.append(('duplicate id', old_trans_id, trans_id))
539
def _parent_type_conflicts(self, by_parent):
540
"""parents must have directory 'contents'."""
542
for parent_id, children in by_parent.iteritems():
543
if parent_id is ROOT_PARENT:
545
if not self._any_contents(children):
547
for child in children:
549
self.final_kind(child)
553
kind = self.final_kind(parent_id)
557
conflicts.append(('missing parent', parent_id))
558
elif kind != "directory":
559
conflicts.append(('non-directory parent', parent_id))
562
def _any_contents(self, trans_ids):
563
"""Return true if any of the trans_ids, will have contents."""
564
for trans_id in trans_ids:
566
kind = self.final_kind(trans_id)
574
Apply all changes to the inventory and filesystem.
576
If filesystem or inventory conflicts are present, MalformedTransform
579
conflicts = self.find_conflicts()
580
if len(conflicts) != 0:
581
raise MalformedTransform(conflicts=conflicts)
583
inv = self._tree.inventory
584
self._apply_removals(inv, limbo_inv)
585
self._apply_insertions(inv, limbo_inv)
586
self._tree._write_inventory(inv)
590
def _limbo_name(self, trans_id):
591
"""Generate the limbo name of a file"""
592
return os.path.join(self._limbodir, trans_id)
594
def _apply_removals(self, inv, limbo_inv):
595
"""Perform tree operations that remove directory/inventory names.
597
That is, delete files that are to be deleted, and put any files that
598
need renaming into limbo. This must be done in strict child-to-parent
601
tree_paths = list(self._tree_path_ids.iteritems())
602
tree_paths.sort(reverse=True)
603
for path, trans_id in tree_paths:
604
full_path = self._tree.abspath(path)
605
if trans_id in self._removed_contents:
606
self.delete_any(full_path)
607
elif trans_id in self._new_name or trans_id in self._new_parent:
609
os.rename(full_path, self._limbo_name(trans_id))
611
if e.errno != errno.ENOENT:
613
if trans_id in self._removed_id:
614
if trans_id == self._new_root:
615
file_id = self._tree.inventory.root.file_id
617
file_id = self.get_tree_file_id(trans_id)
619
elif trans_id in self._new_name or trans_id in self._new_parent:
620
file_id = self.get_tree_file_id(trans_id)
621
if file_id is not None:
622
limbo_inv[trans_id] = inv[file_id]
625
def _apply_insertions(self, inv, limbo_inv):
626
"""Perform tree operations that insert directory/inventory names.
628
That is, create any files that need to be created, and restore from
629
limbo any files that needed renaming. This must be done in strict
630
parent-to-child order.
632
for path, trans_id in self.new_paths():
634
kind = self._new_contents[trans_id]
636
kind = contents = None
637
if trans_id in self._new_contents or self.path_changed(trans_id):
638
full_path = self._tree.abspath(path)
640
os.rename(self._limbo_name(trans_id), full_path)
642
# We may be renaming a dangling inventory id
643
if e.errno != errno.ENOENT:
645
if trans_id in self._new_contents:
646
del self._new_contents[trans_id]
648
if trans_id in self._new_id:
650
kind = file_kind(self._tree.abspath(path))
652
inv.add_path(path, kind, self._new_id[trans_id])
654
raise repr((path, kind, self._new_id[trans_id]))
655
elif trans_id in self._new_name or trans_id in self._new_parent:
656
entry = limbo_inv.get(trans_id)
657
if entry is not None:
658
entry.name = self.final_name(trans_id)
659
parent_trans_id = self.final_parent(trans_id)
660
entry.parent_id = self.final_file_id(parent_trans_id)
663
# requires files and inventory entries to be in place
664
if trans_id in self._new_executability:
665
self._set_executability(path, inv, trans_id)
667
def _set_executability(self, path, inv, trans_id):
668
"""Set the executability of versioned files """
669
file_id = inv.path2id(path)
670
new_executability = self._new_executability[trans_id]
671
inv[file_id].executable = new_executability
672
if supports_executable():
673
abspath = self._tree.abspath(path)
674
current_mode = os.stat(abspath).st_mode
675
if new_executability:
678
to_mode = current_mode | (0100 & ~umask)
679
# Enable x-bit for others only if they can read it.
680
if current_mode & 0004:
681
to_mode |= 0001 & ~umask
682
if current_mode & 0040:
683
to_mode |= 0010 & ~umask
685
to_mode = current_mode & ~0111
686
os.chmod(abspath, to_mode)
688
def _new_entry(self, name, parent_id, file_id):
689
"""Helper function to create a new filesystem entry."""
690
trans_id = self.create_path(name, parent_id)
691
if file_id is not None:
692
self.version_file(file_id, trans_id)
695
def new_file(self, name, parent_id, contents, file_id=None,
698
Convenience method to create files.
700
name is the name of the file to create.
701
parent_id is the transaction id of the parent directory of the file.
702
contents is an iterator of bytestrings, which will be used to produce
704
file_id is the inventory ID of the file, if it is to be versioned.
706
trans_id = self._new_entry(name, parent_id, file_id)
707
self.create_file(contents, trans_id)
708
if executable is not None:
709
self.set_executability(executable, trans_id)
712
def new_directory(self, name, parent_id, file_id=None):
714
Convenience method to create directories.
716
name is the name of the directory to create.
717
parent_id is the transaction id of the parent directory of the
719
file_id is the inventory ID of the directory, if it is to be versioned.
721
trans_id = self._new_entry(name, parent_id, file_id)
722
self.create_directory(trans_id)
725
def new_symlink(self, name, parent_id, target, file_id=None):
727
Convenience method to create symbolic link.
729
name is the name of the symlink to create.
730
parent_id is the transaction id of the parent directory of the symlink.
731
target is a bytestring of the target of the symlink.
732
file_id is the inventory ID of the file, if it is to be versioned.
734
trans_id = self._new_entry(name, parent_id, file_id)
735
self.create_symlink(target, trans_id)
738
def joinpath(parent, child):
739
"""Join tree-relative paths, handling the tree root specially"""
740
if parent is None or parent == "":
743
return os.path.join(parent, child)
745
class FinalPaths(object):
747
Make path calculation cheap by memoizing paths.
749
The underlying tree must not be manipulated between calls, or else
750
the results will likely be incorrect.
752
def __init__(self, root, transform):
753
object.__init__(self)
755
self._known_paths = {}
756
self.transform = transform
758
def _determine_path(self, trans_id):
759
if trans_id == self.root:
761
name = self.transform.final_name(trans_id)
762
parent_id = self.transform.final_parent(trans_id)
763
if parent_id == self.root:
766
return os.path.join(self.get_path(parent_id), name)
768
def get_path(self, trans_id):
769
if trans_id not in self._known_paths:
770
self._known_paths[trans_id] = self._determine_path(trans_id)
771
return self._known_paths[trans_id]
773
def topology_sorted_ids(tree):
774
"""Determine the topological order of the ids in a tree"""
775
file_ids = list(tree)
776
file_ids.sort(key=tree.id2path)
779
def build_tree(branch, tree):
780
"""Create working tree for a branch, using a Transaction."""
782
wt = branch.working_tree()
783
tt = TreeTransform(wt)
785
file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
786
file_ids = topology_sorted_ids(tree)
787
for file_id in file_ids:
788
entry = tree.inventory[file_id]
789
if entry.parent_id is None:
791
if entry.parent_id not in file_trans_id:
792
raise repr(entry.parent_id)
793
parent_id = file_trans_id[entry.parent_id]
794
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
799
def new_by_entry(tt, entry, parent_id, tree):
803
contents = tree.get_file(entry.file_id).readlines()
804
executable = tree.is_executable(entry.file_id)
805
return tt.new_file(name, parent_id, contents, entry.file_id,
807
elif kind == 'directory':
808
return tt.new_directory(name, parent_id, entry.file_id)
809
elif kind == 'symlink':
810
target = entry.get_symlink_target(file_id)
811
return tt.new_symlink(name, parent_id, target, file_id)
813
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
814
if entry.kind == "file":
816
lines = tree.get_file(entry.file_id).readlines()
817
tt.create_file(lines, trans_id, mode_id=mode_id)
818
elif entry.kind == "symlink":
819
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
820
elif entry.kind == "directory":
821
tt.create_directory(trans_id)
823
def create_entry_executability(tt, entry, trans_id):
824
if entry.kind == "file":
825
tt.set_executability(entry.executable, trans_id)
827
def find_interesting(working_tree, target_tree, filenames):
829
interesting_ids = None
831
interesting_ids = set()
832
for tree_path in filenames:
833
for tree in (working_tree, target_tree):
835
file_id = tree.inventory.path2id(tree_path)
836
if file_id is not None:
837
interesting_ids.add(file_id)
840
raise NotVersionedError(path=tree_path)
841
return interesting_ids
844
def change_entry(tt, file_id, working_tree, target_tree,
845
get_trans_id, backups, trans_id):
846
e_trans_id = get_trans_id(file_id)
847
entry = target_tree.inventory[file_id]
848
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
854
tt.delete_contents(e_trans_id)
856
parent_trans_id = get_trans_id(entry.parent_id)
857
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
858
tt.unversion_file(e_trans_id)
859
e_trans_id = tt.create_path(entry.name, parent_trans_id)
860
tt.version_file(file_id, e_trans_id)
861
trans_id[file_id] = e_trans_id
862
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
863
create_entry_executability(tt, entry, e_trans_id)
866
tt.set_executability(entry.executable, e_trans_id)
867
if tt.final_name(e_trans_id) != entry.name:
870
parent_id = tt.final_parent(e_trans_id)
871
parent_file_id = tt.final_file_id(parent_id)
872
if parent_file_id != entry.parent_id:
877
parent_trans_id = get_trans_id(entry.parent_id)
878
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
881
def _entry_changes(file_id, entry, working_tree):
883
Determine in which ways the inventory entry has changed.
885
Returns booleans: has_contents, content_mod, meta_mod
886
has_contents means there are currently contents, but they differ
887
contents_mod means contents need to be modified
888
meta_mod means the metadata needs to be modified
890
cur_entry = working_tree.inventory[file_id]
892
working_kind = working_tree.kind(file_id)
895
if e.errno != errno.ENOENT:
900
if has_contents is True:
901
real_e_kind = entry.kind
902
if real_e_kind == 'root_directory':
903
real_e_kind = 'directory'
904
if real_e_kind != working_kind:
905
contents_mod, meta_mod = True, False
907
cur_entry._read_tree_state(working_tree.id2path(file_id),
909
contents_mod, meta_mod = entry.detect_changes(cur_entry)
910
return has_contents, contents_mod, meta_mod
913
def revert(working_tree, target_tree, filenames, backups=False):
914
interesting_ids = find_interesting(working_tree, target_tree, filenames)
915
def interesting(file_id):
916
return interesting_ids is None or file_id in interesting_ids
918
tt = TreeTransform(working_tree)
921
def get_trans_id(file_id):
923
return trans_id[file_id]
925
return tt.get_id_tree(file_id)
927
for file_id in topology_sorted_ids(target_tree):
928
if not interesting(file_id):
930
if file_id not in working_tree.inventory:
931
entry = target_tree.inventory[file_id]
932
parent_id = get_trans_id(entry.parent_id)
933
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
934
trans_id[file_id] = e_trans_id
936
change_entry(tt, file_id, working_tree, target_tree,
937
get_trans_id, backups, trans_id)
938
for file_id in working_tree:
939
if not interesting(file_id):
941
if file_id not in target_tree:
942
tt.unversion_file(tt.get_id_tree(file_id))
943
resolve_conflicts(tt)
949
def resolve_conflicts(tt):
950
"""Make many conflict-resolution attempts, but die if they fail"""
952
conflicts = tt.find_conflicts()
953
if len(conflicts) == 0:
955
conflict_pass(tt, conflicts)
956
raise MalformedTransform(conflicts=conflicts)
959
def conflict_pass(tt, conflicts):
960
for c_type, conflict in ((c[0], c) for c in conflicts):
961
if c_type == 'duplicate id':
962
tt.unversion_file(conflict[1])
963
elif c_type == 'duplicate':
964
# files that were renamed take precedence
965
new_name = tt.final_name(conflict[1])+'.moved'
966
final_parent = tt.final_parent(conflict[1])
967
if tt.path_changed(conflict[1]):
968
tt.adjust_path(new_name, final_parent, conflict[2])
970
tt.adjust_path(new_name, final_parent, conflict[1])
971
elif c_type == 'parent loop':
972
# break the loop by undoing one of the ops that caused the loop
974
while not tt.path_changed(cur):
975
cur = tt.final_parent(cur)
976
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
977
elif c_type == 'missing parent':
978
trans_id = conflict[1]
980
tt.cancel_deletion(trans_id)
982
tt.create_directory(trans_id)
983
elif c_type == 'unversioned parent':
984
tt.version_file(tt.get_tree_file_id(conflict[1]), conflict[1])
987
class Merge3Merger(object):
989
supports_reprocess = True
990
supports_show_base = True
991
history_based = False
992
def __init__(self, working_tree, this_tree, base_tree, other_tree,
993
reprocess=False, show_base=False):
994
object.__init__(self)
995
self.this_tree = working_tree
996
self.base_tree = base_tree
997
self.other_tree = other_tree
999
self.reprocess = reprocess
1000
self.show_base = show_base
1002
all_ids = set(base_tree)
1003
all_ids.update(other_tree)
1004
self.tt = TreeTransform(working_tree)
1006
for file_id in all_ids:
1007
self.merge_names(file_id)
1008
file_status = self.merge_contents(file_id)
1009
self.merge_executable(file_id, file_status)
1011
resolve_conflicts(self.tt)
1020
def parent(entry, file_id):
1023
return entry.parent_id
1026
def name(entry, file_id):
1032
def contents_sha1(tree, file_id):
1033
if file_id not in tree:
1035
return tree.get_file_sha1(file_id)
1038
def executable(tree, file_id):
1039
if file_id not in tree:
1041
if tree.kind(file_id) != "file":
1043
return tree.is_executable(file_id)
1046
def kind(tree, file_id):
1047
if file_id not in tree:
1049
return tree.kind(file_id)
1052
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1053
"""Do a three-way test on a scalar.
1054
Return "this", "other" or "conflict", depending whether a value wins.
1056
key_base = key(base_tree, file_id)
1057
key_other = key(other_tree, file_id)
1058
#if base == other, either they all agree, or only THIS has changed.
1059
if key_base == key_other:
1061
key_this = key(this_tree, file_id)
1062
if key_this not in (key_base, key_other):
1064
# "Ambiguous clean merge"
1065
elif key_this == key_other:
1068
assert key_this == key_base
1071
def merge_names(self, file_id):
1072
def get_entry(tree):
1073
if file_id in tree.inventory:
1074
return tree.inventory[file_id]
1077
this_entry = get_entry(self.this_tree)
1078
other_entry = get_entry(self.other_tree)
1079
base_entry = get_entry(self.base_tree)
1080
name_winner = self.scalar_three_way(this_entry, base_entry,
1081
other_entry, file_id, self.name)
1082
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
1083
other_entry, file_id,
1085
if this_entry is None:
1086
if name_winner == "this":
1087
name_winner = "other"
1088
if parent_id_winner == "this":
1089
parent_id_winner = "other"
1090
if name_winner == "this" and parent_id_winner == "this":
1092
if name_winner == "conflict":
1093
trans_id = self.tt.get_trans_id(file_id)
1094
self.conflicts.append(('name conflict', trans_id,
1095
self.name(this_entry, file_id),
1096
self.name(other_entry, file_id)))
1097
if parent_id_winner == "conflict":
1098
trans_id = self.tt.get_trans_id(file_id)
1099
self.conflicts.append(('parent conflict', trans_id,
1100
self.parent(this_entry, file_id),
1101
self.parent(other_entry, file_id)))
1102
if other_entry is None:
1103
# it doesn't matter whether the result was 'other' or
1104
# 'conflict'-- if there's no 'other', we leave it alone.
1106
# if we get here, name_winner and parent_winner are set to safe values.
1107
winner_entry = {"this": this_entry, "other": other_entry,
1108
"conflict": other_entry}
1109
trans_id = self.tt.get_trans_id(file_id)
1110
parent_id = winner_entry[parent_id_winner].parent_id
1111
parent_trans_id = self.tt.get_trans_id(parent_id)
1112
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
1116
def merge_contents(self, file_id):
1117
def contents_pair(tree):
1118
if file_id not in tree:
1120
kind = tree.kind(file_id)
1122
contents = tree.get_file_sha1(file_id)
1123
elif kind == "symlink":
1124
contents = tree.get_symlink_target(file_id)
1127
return kind, contents
1128
# See SPOT run. run, SPOT, run.
1129
# So we're not QUITE repeating ourselves; we do tricky things with
1131
base_pair = contents_pair(self.base_tree)
1132
other_pair = contents_pair(self.other_tree)
1133
if base_pair == other_pair:
1135
this_pair = contents_pair(self.this_tree)
1136
if this_pair == other_pair:
1139
trans_id = self.tt.get_trans_id(file_id)
1140
if this_pair == base_pair:
1141
if file_id in self.this_tree:
1142
self.tt.delete_contents(trans_id)
1143
if file_id in self.other_tree.inventory:
1144
create_by_entry(self.tt,
1145
self.other_tree.inventory[file_id],
1146
self.other_tree, trans_id)
1148
if file_id in self.this_tree:
1149
self.tt.unversion_file(trans_id)
1151
elif this_pair[0] == "file" and other_pair[0] == "file":
1152
# If this and other are both files, either base is a file, or
1153
# both converted to files, so at least we have agreement that
1154
# output should be a file.
1155
self.text_merge(file_id, trans_id)
1158
trans_id = self.tt.get_trans_id(file_id)
1159
name = self.tt.final_name(trans_id)
1160
parent_id = self.tt.final_parent(trans_id)
1161
if file_id in self.this_tree.inventory:
1162
self.tt.unversion_file(trans_id)
1163
self.tt.delete_contents(trans_id)
1165
self.tt.cancel_versioning(trans_id)
1166
self.conflicts.append(('contents conflict', (file_id)))
1167
file_group = self._dump_conflicts(name, parent_id, file_id,
1170
def get_lines(self, tree, file_id):
1172
return tree.get_file(file_id).readlines()
1176
def text_merge(self, file_id, trans_id):
1177
"""Perform a three-way text merge on a file_id"""
1178
# it's possible that we got here with base as a different type.
1179
# if so, we just want two-way text conflicts.
1180
if file_id in self.base_tree and \
1181
self.base_tree.kind(file_id) == "file":
1182
base_lines = self.get_lines(self.base_tree, file_id)
1185
other_lines = self.get_lines(self.other_tree, file_id)
1186
this_lines = self.get_lines(self.this_tree, file_id)
1187
m3 = Merge3(base_lines, this_lines, other_lines)
1188
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1189
if self.show_base is True:
1190
base_marker = '|' * 7
1194
def iter_merge3(retval):
1195
retval["text_conflicts"] = False
1196
for line in m3.merge_lines(name_a = "TREE",
1197
name_b = "MERGE-SOURCE",
1198
name_base = "BASE-REVISION",
1199
start_marker=start_marker,
1200
base_marker=base_marker,
1201
reprocess=self.reprocess):
1202
if line.startswith(start_marker):
1203
retval["text_conflicts"] = True
1204
yield line.replace(start_marker, '<' * 7)
1208
merge3_iterator = iter_merge3(retval)
1209
self.tt.create_file(merge3_iterator, trans_id)
1210
if retval["text_conflicts"] is True:
1211
self.conflicts.append(('text conflict', (file_id)))
1212
name = self.tt.final_name(trans_id)
1213
parent_id = self.tt.final_parent(trans_id)
1214
file_group = self._dump_conflicts(name, parent_id, file_id,
1215
this_lines, base_lines,
1217
file_group.append(trans_id)
1219
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
1220
base_lines=None, other_lines=None, set_version=False,
1222
data = [('OTHER', self.other_tree, other_lines),
1223
('THIS', self.this_tree, this_lines)]
1225
data.append(('BASE', self.base_tree, base_lines))
1228
for suffix, tree, lines in data:
1230
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1232
file_group.append(trans_id)
1233
if set_version and not versioned:
1234
self.tt.version_file(file_id, trans_id)
1238
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1240
name = name + '.' + suffix
1241
trans_id = self.tt.create_path(name, parent_id)
1242
entry = tree.inventory[file_id]
1243
create_by_entry(self.tt, entry, tree, trans_id, lines)
1246
def merge_executable(self, file_id, file_status):
1247
if file_status == "deleted":
1249
trans_id = self.tt.get_trans_id(file_id)
1251
if self.tt.final_kind(trans_id) != "file":
1255
winner = self.scalar_three_way(self.this_tree, self.base_tree,
1256
self.other_tree, file_id,
1258
if winner == "conflict":
1259
# There must be a None in here, if we have a conflict, but we
1260
# need executability since file status was not deleted.
1261
if self.other_tree.is_executable(file_id) is None:
1265
if winner == "this":
1266
if file_status == "modified":
1267
executability = self.this_tree.is_executable(file_id)
1268
if executability is not None:
1269
trans_id = self.tt.get_trans_id(file_id)
1270
self.tt.set_executability(executability, trans_id)
1272
assert winner == "other"
1273
if file_id in self.other_tree:
1274
executability = self.other_tree.is_executable(file_id)
1275
elif file_id in self.this_tree:
1276
executability = self.this_tree.is_executable(file_id)
1277
elif file_id in self.base_tree:
1278
executability = self.base_tree.is_executable(file_id)
1279
if executability is not None:
1280
trans_id = self.tt.get_trans_id(file_id)
1281
self.tt.set_executability(executability, trans_id)
1283
class WeaveMerger(Merge3Merger):
1284
supports_reprocess = False
1285
supports_show_base = False
1286
def _merged_lines(self, file_id):
1287
"""Generate the merged lines.
1288
There is no distinction between lines that are meant to contain <<<<<<<
1291
if getattr(self.this_tree, 'get_weave', False) is False:
1292
# If we have a WorkingTree, try using the basis
1293
wt_sha1 = self.this_tree.get_file_sha1(file_id)
1294
this_tree = self.this_tree.branch.basis_tree()
1295
if this_tree.get_file_sha1(file_id) != wt_sha1:
1296
raise WorkingTreeNotRevision(self.this_tree)
1298
this_tree = self.this_tree
1299
weave = this_tree.get_weave(file_id)
1300
this_revision_id = this_tree.inventory[file_id].revision
1301
other_revision_id = self.other_tree.inventory[file_id].revision
1302
this_i = weave.lookup(this_revision_id)
1303
other_i = weave.lookup(other_revision_id)
1304
plan = weave.plan_merge(this_i, other_i)
1305
return weave.weave_merge(plan)
1307
def text_merge(self, file_id, trans_id):
1308
lines = self._merged_lines(file_id)
1309
conflicts = '<<<<<<<\n' in lines
1310
self.tt.create_file(lines, trans_id)
1312
self.conflicts.append(('text conflict', (file_id)))
1313
name = self.tt.final_name(trans_id)
1314
parent_id = self.tt.final_parent(trans_id)
1315
file_group = self._dump_conflicts(name, parent_id, file_id,
1317
file_group.append(trans_id)
1320
class Diff3Merger(Merge3Merger):
1321
"""Use good ol' diff3 to do text merges"""
1322
def dump_file(self, temp_dir, name, tree, file_id):
1323
out_path = pathjoin(temp_dir, name)
1324
out_file = file(out_path, "wb")
1325
in_file = tree.get_file(file_id)
1326
for line in in_file:
1327
out_file.write(line)
1330
def text_merge(self, file_id, trans_id):
1332
temp_dir = mkdtemp(prefix="bzr-")
1334
new_file = os.path.join(temp_dir, "new")
1335
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1336
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1337
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1338
status = bzrlib.patch.diff3(new_file, this, base, other)
1339
if status not in (0, 1):
1340
raise BzrError("Unhandled diff3 exit code")
1341
self.tt.create_file(file(new_file, "rb"), trans_id)
1343
name = self.tt.final_name(trans_id)
1344
parent_id = self.tt.final_parent(trans_id)
1345
self._dump_conflicts(name, parent_id, file_id)
1346
self.conflicts.append(('text conflict', (file_id)))