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 stat import S_ISREG
21
from bzrlib import BZRDIR
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
23
ReusingTransform, NotVersionedError, CantMoveRoot)
24
from bzrlib.inventory import InventoryEntry
25
from bzrlib.osutils import file_kind, supports_executable, pathjoin
26
from bzrlib.trace import mutter
28
ROOT_PARENT = "root-parent"
30
def unique_add(map, key, value):
32
raise DuplicateKey(key=key)
35
class TreeTransform(object):
36
"""Represent a tree transformation."""
37
def __init__(self, tree):
38
"""Note: a write lock is taken on the tree.
40
Use TreeTransform.finalize() to release the lock
44
self._tree.lock_write()
48
self._new_contents = {}
49
self._removed_contents = set()
50
self._new_executability = {}
52
self._non_present_ids = {}
54
self._removed_id = set()
55
self._tree_path_ids = {}
56
self._tree_id_paths = {}
57
self._new_root = self.get_id_tree(tree.get_root_id())
59
# XXX use the WorkingTree LockableFiles, when available
60
control_files = self._tree.branch.control_files
61
self._limbodir = control_files.controlfilename('limbo')
62
os.mkdir(self._limbodir)
67
root = property(__get_root)
70
"""Release the working tree lock, if held."""
71
if self._tree is None:
73
for trans_id, kind in self._new_contents.iteritems():
74
path = self._limbo_name(trans_id)
75
if kind == "directory":
79
os.rmdir(self._limbodir)
84
"""Produce a new tranform id"""
85
new_id = "new-%s" % self._id_number
89
def create_path(self, name, parent):
90
"""Assign a transaction id to a new path"""
91
trans_id = self._assign_id()
92
unique_add(self._new_name, trans_id, name)
93
unique_add(self._new_parent, trans_id, parent)
96
def adjust_path(self, name, parent, trans_id):
97
"""Change the path that is assigned to a transaction id."""
98
if trans_id == self._new_root:
100
self._new_name[trans_id] = name
101
self._new_parent[trans_id] = parent
103
def adjust_root_path(self, name, parent):
104
"""Emulate moving the root by moving all children, instead.
106
We do this by undoing the association of root's transaction id with the
107
current tree. This allows us to create a new directory with that
108
transaction id. We unversion the root directory and version the
109
physically new directory, and hope someone versions the tree root
112
old_root = self._new_root
113
old_root_file_id = self.final_file_id(old_root)
114
# force moving all children of root
115
for child_id in self.iter_tree_children(old_root):
116
if child_id != parent:
117
self.adjust_path(self.final_name(child_id),
118
self.final_parent(child_id), child_id)
119
file_id = self.final_file_id(child_id)
120
if file_id is not None:
121
self.unversion_file(child_id)
122
self.version_file(file_id, child_id)
124
# the physical root needs a new transaction id
125
self._tree_path_ids.pop("")
126
self._tree_id_paths.pop(old_root)
127
self._new_root = self.get_id_tree(self._tree.get_root_id())
128
if parent == old_root:
129
parent = self._new_root
130
self.adjust_path(name, parent, old_root)
131
self.create_directory(old_root)
132
self.version_file(old_root_file_id, old_root)
133
self.unversion_file(self._new_root)
135
def get_id_tree(self, inventory_id):
136
"""Determine the transaction id of a working tree file.
138
This reflects only files that already exist, not ones that will be
139
added by transactions.
141
path = self._tree.inventory.id2path(inventory_id)
142
return self.get_tree_path_id(path)
144
def get_trans_id(self, file_id):
146
Determine or set the transaction id associated with a file ID.
147
A new id is only created for file_ids that were never present. If
148
a transaction has been unversioned, it is deliberately still returned.
149
(this will likely lead to an unversioned parent conflict.)
151
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
152
return self._r_new_id[file_id]
153
elif file_id in self._tree.inventory:
154
return self.get_id_tree(file_id)
155
elif file_id in self._non_present_ids:
156
return self._non_present_ids[file_id]
158
trans_id = self._assign_id()
159
self._non_present_ids[file_id] = trans_id
162
def canonical_path(self, path):
163
"""Get the canonical tree-relative path"""
164
# don't follow final symlinks
165
dirname, basename = os.path.split(self._tree.abspath(path))
166
dirname = os.path.realpath(dirname)
167
return self._tree.relpath(os.path.join(dirname, basename))
169
def get_tree_path_id(self, path):
170
"""Determine (and maybe set) the transaction ID for a tree path."""
171
path = self.canonical_path(path)
172
if path not in self._tree_path_ids:
173
self._tree_path_ids[path] = self._assign_id()
174
self._tree_id_paths[self._tree_path_ids[path]] = path
175
return self._tree_path_ids[path]
177
def get_tree_parent(self, trans_id):
178
"""Determine id of the parent in the tree."""
179
path = self._tree_id_paths[trans_id]
182
return self.get_tree_path_id(os.path.dirname(path))
184
def create_file(self, contents, trans_id, mode_id=None):
185
"""Schedule creation of a new file.
189
Contents is an iterator of strings, all of which will be written
190
to the target destination.
192
New file takes the permissions of any existing file with that id,
193
unless mode_id is specified.
195
f = file(self._limbo_name(trans_id), 'wb')
196
unique_add(self._new_contents, trans_id, 'file')
197
for segment in contents:
200
self._set_mode(trans_id, mode_id, S_ISREG)
202
def _set_mode(self, trans_id, mode_id, typefunc):
206
old_path = self._tree_id_paths[mode_id]
210
mode = os.stat(old_path).st_mode
212
if e.errno == errno.ENOENT:
217
os.chmod(self._limbo_name(trans_id), mode)
219
def create_directory(self, trans_id):
220
"""Schedule creation of a new directory.
222
See also new_directory.
224
os.mkdir(self._limbo_name(trans_id))
225
unique_add(self._new_contents, trans_id, 'directory')
227
def create_symlink(self, target, trans_id):
228
"""Schedule creation of a new symbolic link.
230
target is a bytestring.
231
See also new_symlink.
233
os.symlink(target, self._limbo_name(trans_id))
234
unique_add(self._new_contents, trans_id, 'symlink')
237
def delete_any(full_path):
241
# We may be renaming a dangling inventory id
242
if e.errno != errno.EISDIR and e.errno != errno.EACCES:
246
def cancel_creation(self, trans_id):
247
del self._new_contents[trans_id]
248
self.delete_any(self._limbo_name(trans_id))
250
def delete_contents(self, trans_id):
251
"""Schedule the contents of a path entry for deletion"""
252
self.tree_kind(trans_id)
253
self._removed_contents.add(trans_id)
255
def cancel_deletion(self, trans_id):
256
"""Cancel a scheduled deletion"""
257
self._removed_contents.remove(trans_id)
259
def unversion_file(self, trans_id):
260
"""Schedule a path entry to become unversioned"""
261
self._removed_id.add(trans_id)
263
def delete_versioned(self, trans_id):
264
"""Delete and unversion a versioned file"""
265
self.delete_contents(trans_id)
266
self.unversion_file(trans_id)
268
def set_executability(self, executability, trans_id):
269
"""Schedule setting of the 'execute' bit"""
270
if executability is None:
271
del self._new_executability[trans_id]
273
unique_add(self._new_executability, trans_id, executability)
275
def version_file(self, file_id, trans_id):
276
"""Schedule a file to become versioned."""
277
assert file_id is not None
278
unique_add(self._new_id, trans_id, file_id)
279
unique_add(self._r_new_id, file_id, trans_id)
281
def cancel_versioning(self, trans_id):
282
"""Undo a previous versioning of a file"""
283
file_id = self._new_id[trans_id]
284
del self._new_id[trans_id]
285
del self._r_new_id[file_id]
288
"""Determine the paths of all new and changed files"""
290
fp = FinalPaths(self)
291
for id_set in (self._new_name, self._new_parent, self._new_contents,
292
self._new_id, self._new_executability):
293
new_ids.update(id_set)
294
new_paths = [(fp.get_path(t), t) for t in new_ids]
298
def tree_kind(self, trans_id):
299
"""Determine the file kind in the working tree.
301
Raises NoSuchFile if the file does not exist
303
path = self._tree_id_paths.get(trans_id)
305
raise NoSuchFile(None)
307
return file_kind(self._tree.abspath(path))
309
if e.errno != errno.ENOENT:
312
raise NoSuchFile(path)
314
def final_kind(self, trans_id):
316
Determine the final file kind, after any changes applied.
318
Raises NoSuchFile if the file does not exist/has no contents.
319
(It is conceivable that a path would be created without the
320
corresponding contents insertion command)
322
if trans_id in self._new_contents:
323
return self._new_contents[trans_id]
324
elif trans_id in self._removed_contents:
325
raise NoSuchFile(None)
327
return self.tree_kind(trans_id)
329
def get_tree_file_id(self, trans_id):
330
"""Determine the file id associated with the trans_id in the tree"""
332
path = self._tree_id_paths[trans_id]
334
# the file is a new, unversioned file, or invalid trans_id
336
# the file is old; the old id is still valid
337
if self._new_root == trans_id:
338
return self._tree.inventory.root.file_id
339
return self._tree.inventory.path2id(path)
341
def final_file_id(self, trans_id):
343
Determine the file id after any changes are applied, or None.
345
None indicates that the file will not be versioned after changes are
349
# there is a new id for this file
350
assert self._new_id[trans_id] is not None
351
return self._new_id[trans_id]
353
if trans_id in self._removed_id:
355
return self.get_tree_file_id(trans_id)
358
def inactive_file_id(self, trans_id):
359
"""Returns the inactive file_id associated with a transaction id.
360
That is, the one in the tree or in non_present_ids.
361
The file_id may actually be active, too.
363
file_id = self.get_tree_file_id(trans_id)
364
if file_id is not None:
366
for key, value in self._non_present_ids.iteritems():
367
if value == trans_id:
371
def final_parent(self, trans_id):
373
Determine the parent file_id, after any changes are applied.
375
ROOT_PARENT is returned for the tree root.
378
return self._new_parent[trans_id]
380
return self.get_tree_parent(trans_id)
382
def final_name(self, trans_id):
383
"""Determine the final filename, after all changes are applied."""
385
return self._new_name[trans_id]
387
return os.path.basename(self._tree_id_paths[trans_id])
389
def _by_parent(self):
390
"""Return a map of parent: children for known parents.
392
Only new paths and parents of tree files with assigned ids are used.
395
items = list(self._new_parent.iteritems())
396
items.extend((t, self.final_parent(t)) for t in
397
self._tree_id_paths.keys())
398
for trans_id, parent_id in items:
399
if parent_id not in by_parent:
400
by_parent[parent_id] = set()
401
by_parent[parent_id].add(trans_id)
404
def path_changed(self, trans_id):
405
return trans_id in self._new_name or trans_id in self._new_parent
407
def find_conflicts(self):
408
"""Find any violations of inventory or filesystem invariants"""
409
if self.__done is True:
410
raise ReusingTransform()
412
# ensure all children of all existent parents are known
413
# all children of non-existent parents are known, by definition.
414
self._add_tree_children()
415
by_parent = self._by_parent()
416
conflicts.extend(self._unversioned_parents(by_parent))
417
conflicts.extend(self._parent_loops())
418
conflicts.extend(self._duplicate_entries(by_parent))
419
conflicts.extend(self._duplicate_ids())
420
conflicts.extend(self._parent_type_conflicts(by_parent))
421
conflicts.extend(self._improper_versioning())
422
conflicts.extend(self._executability_conflicts())
425
def _add_tree_children(self):
427
Add all the children of all active parents to the known paths.
429
Active parents are those which gain children, and those which are
430
removed. This is a necessary first step in detecting conflicts.
432
parents = self._by_parent().keys()
433
parents.extend([t for t in self._removed_contents if
434
self.tree_kind(t) == 'directory'])
435
for trans_id in self._removed_id:
436
file_id = self.get_tree_file_id(trans_id)
437
if self._tree.inventory[file_id].kind in ('directory',
439
parents.append(trans_id)
441
for parent_id in parents:
442
# ensure that all children are registered with the transaction
443
list(self.iter_tree_children(parent_id))
445
def iter_tree_children(self, parent_id):
446
"""Iterate through the entry's tree children, if any"""
448
path = self._tree_id_paths[parent_id]
452
children = os.listdir(self._tree.abspath(path))
454
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
458
for child in children:
459
childpath = joinpath(path, child)
460
if childpath == BZRDIR:
462
yield self.get_tree_path_id(childpath)
464
def _parent_loops(self):
465
"""No entry should be its own ancestor"""
467
for trans_id in self._new_parent:
470
while parent_id is not ROOT_PARENT:
472
parent_id = self.final_parent(parent_id)
473
if parent_id == trans_id:
474
conflicts.append(('parent loop', trans_id))
475
if parent_id in seen:
479
def _unversioned_parents(self, by_parent):
480
"""If parent directories are versioned, children must be versioned."""
482
for parent_id, children in by_parent.iteritems():
483
if parent_id is ROOT_PARENT:
485
if self.final_file_id(parent_id) is not None:
487
for child_id in children:
488
if self.final_file_id(child_id) is not None:
489
conflicts.append(('unversioned parent', parent_id))
493
def _improper_versioning(self):
495
Cannot version a file with no contents, or a bad type.
497
However, existing entries with no contents are okay.
500
for trans_id in self._new_id.iterkeys():
502
kind = self.final_kind(trans_id)
504
conflicts.append(('versioning no contents', trans_id))
506
if not InventoryEntry.versionable_kind(kind):
507
conflicts.append(('versioning bad kind', trans_id, kind))
510
def _executability_conflicts(self):
511
"""Check for bad executability changes.
513
Only versioned files may have their executability set, because
514
1. only versioned entries can have executability under windows
515
2. only files can be executable. (The execute bit on a directory
516
does not indicate searchability)
519
for trans_id in self._new_executability:
520
if self.final_file_id(trans_id) is None:
521
conflicts.append(('unversioned executability', trans_id))
524
non_file = self.final_kind(trans_id) != "file"
528
conflicts.append(('non-file executability', trans_id))
531
def _duplicate_entries(self, by_parent):
532
"""No directory may have two entries with the same name."""
534
for children in by_parent.itervalues():
535
name_ids = [(self.final_name(t), t) for t in children]
539
for name, trans_id in name_ids:
540
if name == last_name:
541
conflicts.append(('duplicate', last_trans_id, trans_id,
544
last_trans_id = trans_id
547
def _duplicate_ids(self):
548
"""Each inventory id may only be used once"""
550
removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
552
active_tree_ids = set((f for f in self._tree.inventory if
553
f not in removed_tree_ids))
554
for trans_id, file_id in self._new_id.iteritems():
555
if file_id in active_tree_ids:
556
old_trans_id = self.get_id_tree(file_id)
557
conflicts.append(('duplicate id', old_trans_id, trans_id))
560
def _parent_type_conflicts(self, by_parent):
561
"""parents must have directory 'contents'."""
563
for parent_id, children in by_parent.iteritems():
564
if parent_id is ROOT_PARENT:
566
if not self._any_contents(children):
568
for child in children:
570
self.final_kind(child)
574
kind = self.final_kind(parent_id)
578
conflicts.append(('missing parent', parent_id))
579
elif kind != "directory":
580
conflicts.append(('non-directory parent', parent_id))
583
def _any_contents(self, trans_ids):
584
"""Return true if any of the trans_ids, will have contents."""
585
for trans_id in trans_ids:
587
kind = self.final_kind(trans_id)
595
Apply all changes to the inventory and filesystem.
597
If filesystem or inventory conflicts are present, MalformedTransform
600
conflicts = self.find_conflicts()
601
if len(conflicts) != 0:
602
raise MalformedTransform(conflicts=conflicts)
604
inv = self._tree.inventory
605
self._apply_removals(inv, limbo_inv)
606
self._apply_insertions(inv, limbo_inv)
607
self._tree._write_inventory(inv)
611
def _limbo_name(self, trans_id):
612
"""Generate the limbo name of a file"""
613
return os.path.join(self._limbodir, trans_id)
615
def _apply_removals(self, inv, limbo_inv):
616
"""Perform tree operations that remove directory/inventory names.
618
That is, delete files that are to be deleted, and put any files that
619
need renaming into limbo. This must be done in strict child-to-parent
622
tree_paths = list(self._tree_path_ids.iteritems())
623
tree_paths.sort(reverse=True)
624
for path, trans_id in tree_paths:
625
full_path = self._tree.abspath(path)
626
if trans_id in self._removed_contents:
627
self.delete_any(full_path)
628
elif trans_id in self._new_name or trans_id in self._new_parent:
630
os.rename(full_path, self._limbo_name(trans_id))
632
if e.errno != errno.ENOENT:
634
if trans_id in self._removed_id:
635
if trans_id == self._new_root:
636
file_id = self._tree.inventory.root.file_id
638
file_id = self.get_tree_file_id(trans_id)
640
elif trans_id in self._new_name or trans_id in self._new_parent:
641
file_id = self.get_tree_file_id(trans_id)
642
if file_id is not None:
643
limbo_inv[trans_id] = inv[file_id]
646
def _apply_insertions(self, inv, limbo_inv):
647
"""Perform tree operations that insert directory/inventory names.
649
That is, create any files that need to be created, and restore from
650
limbo any files that needed renaming. This must be done in strict
651
parent-to-child order.
653
for path, trans_id in self.new_paths():
655
kind = self._new_contents[trans_id]
657
kind = contents = None
658
if trans_id in self._new_contents or self.path_changed(trans_id):
659
full_path = self._tree.abspath(path)
661
os.rename(self._limbo_name(trans_id), full_path)
663
# We may be renaming a dangling inventory id
664
if e.errno != errno.ENOENT:
666
if trans_id in self._new_contents:
667
del self._new_contents[trans_id]
669
if trans_id in self._new_id:
671
kind = file_kind(self._tree.abspath(path))
672
inv.add_path(path, kind, self._new_id[trans_id])
673
elif trans_id in self._new_name or trans_id in self._new_parent:
674
entry = limbo_inv.get(trans_id)
675
if entry is not None:
676
entry.name = self.final_name(trans_id)
677
parent_path = os.path.dirname(path)
678
entry.parent_id = self._tree.inventory.path2id(parent_path)
681
# requires files and inventory entries to be in place
682
if trans_id in self._new_executability:
683
self._set_executability(path, inv, trans_id)
685
def _set_executability(self, path, inv, trans_id):
686
"""Set the executability of versioned files """
687
file_id = inv.path2id(path)
688
new_executability = self._new_executability[trans_id]
689
inv[file_id].executable = new_executability
690
if supports_executable():
691
abspath = self._tree.abspath(path)
692
current_mode = os.stat(abspath).st_mode
693
if new_executability:
696
to_mode = current_mode | (0100 & ~umask)
697
# Enable x-bit for others only if they can read it.
698
if current_mode & 0004:
699
to_mode |= 0001 & ~umask
700
if current_mode & 0040:
701
to_mode |= 0010 & ~umask
703
to_mode = current_mode & ~0111
704
os.chmod(abspath, to_mode)
706
def _new_entry(self, name, parent_id, file_id):
707
"""Helper function to create a new filesystem entry."""
708
trans_id = self.create_path(name, parent_id)
709
if file_id is not None:
710
self.version_file(file_id, trans_id)
713
def new_file(self, name, parent_id, contents, file_id=None,
716
Convenience method to create files.
718
name is the name of the file to create.
719
parent_id is the transaction id of the parent directory of the file.
720
contents is an iterator of bytestrings, which will be used to produce
722
file_id is the inventory ID of the file, if it is to be versioned.
724
trans_id = self._new_entry(name, parent_id, file_id)
725
self.create_file(contents, trans_id)
726
if executable is not None:
727
self.set_executability(executable, trans_id)
730
def new_directory(self, name, parent_id, file_id=None):
732
Convenience method to create directories.
734
name is the name of the directory to create.
735
parent_id is the transaction id of the parent directory of the
737
file_id is the inventory ID of the directory, if it is to be versioned.
739
trans_id = self._new_entry(name, parent_id, file_id)
740
self.create_directory(trans_id)
743
def new_symlink(self, name, parent_id, target, file_id=None):
745
Convenience method to create symbolic link.
747
name is the name of the symlink to create.
748
parent_id is the transaction id of the parent directory of the symlink.
749
target is a bytestring of the target of the symlink.
750
file_id is the inventory ID of the file, if it is to be versioned.
752
trans_id = self._new_entry(name, parent_id, file_id)
753
self.create_symlink(target, trans_id)
756
def joinpath(parent, child):
757
"""Join tree-relative paths, handling the tree root specially"""
758
if parent is None or parent == "":
761
return os.path.join(parent, child)
763
class FinalPaths(object):
765
Make path calculation cheap by memoizing paths.
767
The underlying tree must not be manipulated between calls, or else
768
the results will likely be incorrect.
770
def __init__(self, transform):
771
object.__init__(self)
772
self._known_paths = {}
773
self.transform = transform
775
def _determine_path(self, trans_id):
776
if trans_id == self.transform.root:
778
name = self.transform.final_name(trans_id)
779
parent_id = self.transform.final_parent(trans_id)
780
if parent_id == self.transform.root:
783
return os.path.join(self.get_path(parent_id), name)
785
def get_path(self, trans_id):
786
if trans_id not in self._known_paths:
787
self._known_paths[trans_id] = self._determine_path(trans_id)
788
return self._known_paths[trans_id]
790
def topology_sorted_ids(tree):
791
"""Determine the topological order of the ids in a tree"""
792
file_ids = list(tree)
793
file_ids.sort(key=tree.id2path)
796
def build_tree(branch, tree):
797
"""Create working tree for a branch, using a Transaction."""
799
wt = branch.working_tree()
800
tt = TreeTransform(wt)
802
file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
803
file_ids = topology_sorted_ids(tree)
804
for file_id in file_ids:
805
entry = tree.inventory[file_id]
806
if entry.parent_id is None:
808
if entry.parent_id not in file_trans_id:
809
raise repr(entry.parent_id)
810
parent_id = file_trans_id[entry.parent_id]
811
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
816
def new_by_entry(tt, entry, parent_id, tree):
820
contents = tree.get_file(entry.file_id).readlines()
821
executable = tree.is_executable(entry.file_id)
822
return tt.new_file(name, parent_id, contents, entry.file_id,
824
elif kind == 'directory':
825
return tt.new_directory(name, parent_id, entry.file_id)
826
elif kind == 'symlink':
827
target = entry.get_symlink_target(file_id)
828
return tt.new_symlink(name, parent_id, target, file_id)
830
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
831
if entry.kind == "file":
833
lines = tree.get_file(entry.file_id).readlines()
834
tt.create_file(lines, trans_id, mode_id=mode_id)
835
elif entry.kind == "symlink":
836
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
837
elif entry.kind == "directory":
838
tt.create_directory(trans_id)
840
def create_entry_executability(tt, entry, trans_id):
841
if entry.kind == "file":
842
tt.set_executability(entry.executable, trans_id)
844
def find_interesting(working_tree, target_tree, filenames):
846
interesting_ids = None
848
interesting_ids = set()
849
for tree_path in filenames:
850
for tree in (working_tree, target_tree):
852
file_id = tree.inventory.path2id(tree_path)
853
if file_id is not None:
854
interesting_ids.add(file_id)
857
raise NotVersionedError(path=tree_path)
858
return interesting_ids
861
def change_entry(tt, file_id, working_tree, target_tree,
862
get_trans_id, backups, trans_id):
863
e_trans_id = get_trans_id(file_id)
864
entry = target_tree.inventory[file_id]
865
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
871
tt.delete_contents(e_trans_id)
873
parent_trans_id = get_trans_id(entry.parent_id)
874
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
875
tt.unversion_file(e_trans_id)
876
e_trans_id = tt.create_path(entry.name, parent_trans_id)
877
tt.version_file(file_id, e_trans_id)
878
trans_id[file_id] = e_trans_id
879
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
880
create_entry_executability(tt, entry, e_trans_id)
883
tt.set_executability(entry.executable, e_trans_id)
884
if tt.final_name(e_trans_id) != entry.name:
887
parent_id = tt.final_parent(e_trans_id)
888
parent_file_id = tt.final_file_id(parent_id)
889
if parent_file_id != entry.parent_id:
894
parent_trans_id = get_trans_id(entry.parent_id)
895
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
898
def _entry_changes(file_id, entry, working_tree):
900
Determine in which ways the inventory entry has changed.
902
Returns booleans: has_contents, content_mod, meta_mod
903
has_contents means there are currently contents, but they differ
904
contents_mod means contents need to be modified
905
meta_mod means the metadata needs to be modified
907
cur_entry = working_tree.inventory[file_id]
909
working_kind = working_tree.kind(file_id)
912
if e.errno != errno.ENOENT:
917
if has_contents is True:
918
real_e_kind = entry.kind
919
if real_e_kind == 'root_directory':
920
real_e_kind = 'directory'
921
if real_e_kind != working_kind:
922
contents_mod, meta_mod = True, False
924
cur_entry._read_tree_state(working_tree.id2path(file_id),
926
contents_mod, meta_mod = entry.detect_changes(cur_entry)
927
return has_contents, contents_mod, meta_mod
930
def revert(working_tree, target_tree, filenames, backups=False):
931
interesting_ids = find_interesting(working_tree, target_tree, filenames)
932
def interesting(file_id):
933
return interesting_ids is None or file_id in interesting_ids
935
tt = TreeTransform(working_tree)
938
def get_trans_id(file_id):
940
return trans_id[file_id]
942
return tt.get_id_tree(file_id)
944
for file_id in topology_sorted_ids(target_tree):
945
if not interesting(file_id):
947
if file_id not in working_tree.inventory:
948
entry = target_tree.inventory[file_id]
949
parent_id = get_trans_id(entry.parent_id)
950
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
951
trans_id[file_id] = e_trans_id
953
change_entry(tt, file_id, working_tree, target_tree,
954
get_trans_id, backups, trans_id)
955
for file_id in working_tree:
956
if not interesting(file_id):
958
if file_id not in target_tree:
959
tt.unversion_file(tt.get_id_tree(file_id))
960
resolve_conflicts(tt)
966
def resolve_conflicts(tt):
967
"""Make many conflict-resolution attempts, but die if they fail"""
969
conflicts = tt.find_conflicts()
970
if len(conflicts) == 0:
972
conflict_pass(tt, conflicts)
973
raise MalformedTransform(conflicts=conflicts)
976
def conflict_pass(tt, conflicts):
977
for c_type, conflict in ((c[0], c) for c in conflicts):
978
if c_type == 'duplicate id':
979
tt.unversion_file(conflict[1])
980
elif c_type == 'duplicate':
981
# files that were renamed take precedence
982
new_name = tt.final_name(conflict[1])+'.moved'
983
final_parent = tt.final_parent(conflict[1])
984
if tt.path_changed(conflict[1]):
985
tt.adjust_path(new_name, final_parent, conflict[2])
987
tt.adjust_path(new_name, final_parent, conflict[1])
988
elif c_type == 'parent loop':
989
# break the loop by undoing one of the ops that caused the loop
991
while not tt.path_changed(cur):
992
cur = tt.final_parent(cur)
993
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
994
elif c_type == 'missing parent':
995
trans_id = conflict[1]
997
tt.cancel_deletion(trans_id)
999
tt.create_directory(trans_id)
1000
elif c_type == 'unversioned parent':
1001
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])