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, clean up limbo dir."""
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):
145
"""Determine or set the transaction id associated with a file ID.
146
A new id is only created for file_ids that were never present. If
147
a transaction has been unversioned, it is deliberately still returned.
148
(this will likely lead to an unversioned parent conflict.)
150
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
151
return self._r_new_id[file_id]
152
elif file_id in self._tree.inventory:
153
return self.get_id_tree(file_id)
154
elif file_id in self._non_present_ids:
155
return self._non_present_ids[file_id]
157
trans_id = self._assign_id()
158
self._non_present_ids[file_id] = trans_id
161
def canonical_path(self, path):
162
"""Get the canonical tree-relative path"""
163
# don't follow final symlinks
164
dirname, basename = os.path.split(self._tree.abspath(path))
165
dirname = os.path.realpath(dirname)
166
return self._tree.relpath(os.path.join(dirname, basename))
168
def get_tree_path_id(self, path):
169
"""Determine (and maybe set) the transaction ID for a tree path."""
170
path = self.canonical_path(path)
171
if path not in self._tree_path_ids:
172
self._tree_path_ids[path] = self._assign_id()
173
self._tree_id_paths[self._tree_path_ids[path]] = path
174
return self._tree_path_ids[path]
176
def get_tree_parent(self, trans_id):
177
"""Determine id of the parent in the tree."""
178
path = self._tree_id_paths[trans_id]
181
return self.get_tree_path_id(os.path.dirname(path))
183
def create_file(self, contents, trans_id, mode_id=None):
184
"""Schedule creation of a new file.
188
Contents is an iterator of strings, all of which will be written
189
to the target destination.
191
New file takes the permissions of any existing file with that id,
192
unless mode_id is specified.
194
f = file(self._limbo_name(trans_id), 'wb')
195
unique_add(self._new_contents, trans_id, 'file')
196
for segment in contents:
199
self._set_mode(trans_id, mode_id, S_ISREG)
201
def _set_mode(self, trans_id, mode_id, typefunc):
202
"""Set the mode of new file contents.
203
The mode_id is the existing file to get the mode from (often the same
204
as trans_id). The operation is only performed if there's a mode match
205
according to typefunc.
210
old_path = self._tree_id_paths[mode_id]
214
mode = os.stat(old_path).st_mode
216
if e.errno == errno.ENOENT:
221
os.chmod(self._limbo_name(trans_id), mode)
223
def create_directory(self, trans_id):
224
"""Schedule creation of a new directory.
226
See also new_directory.
228
os.mkdir(self._limbo_name(trans_id))
229
unique_add(self._new_contents, trans_id, 'directory')
231
def create_symlink(self, target, trans_id):
232
"""Schedule creation of a new symbolic link.
234
target is a bytestring.
235
See also new_symlink.
237
os.symlink(target, self._limbo_name(trans_id))
238
unique_add(self._new_contents, trans_id, 'symlink')
241
def delete_any(full_path):
242
"""Delete a file or directory."""
246
# We may be renaming a dangling inventory id
247
if e.errno != errno.EISDIR and e.errno != errno.EACCES:
251
def cancel_creation(self, trans_id):
252
"""Cancel the creation of new file contents."""
253
del self._new_contents[trans_id]
254
self.delete_any(self._limbo_name(trans_id))
256
def delete_contents(self, trans_id):
257
"""Schedule the contents of a path entry for deletion"""
258
self.tree_kind(trans_id)
259
self._removed_contents.add(trans_id)
261
def cancel_deletion(self, trans_id):
262
"""Cancel a scheduled deletion"""
263
self._removed_contents.remove(trans_id)
265
def unversion_file(self, trans_id):
266
"""Schedule a path entry to become unversioned"""
267
self._removed_id.add(trans_id)
269
def delete_versioned(self, trans_id):
270
"""Delete and unversion a versioned file"""
271
self.delete_contents(trans_id)
272
self.unversion_file(trans_id)
274
def set_executability(self, executability, trans_id):
275
"""Schedule setting of the 'execute' bit"""
276
if executability is None:
277
del self._new_executability[trans_id]
279
unique_add(self._new_executability, trans_id, executability)
281
def version_file(self, file_id, trans_id):
282
"""Schedule a file to become versioned."""
283
assert file_id is not None
284
unique_add(self._new_id, trans_id, file_id)
285
unique_add(self._r_new_id, file_id, trans_id)
287
def cancel_versioning(self, trans_id):
288
"""Undo a previous versioning of a file"""
289
file_id = self._new_id[trans_id]
290
del self._new_id[trans_id]
291
del self._r_new_id[file_id]
294
"""Determine the paths of all new and changed files"""
296
fp = FinalPaths(self)
297
for id_set in (self._new_name, self._new_parent, self._new_contents,
298
self._new_id, self._new_executability):
299
new_ids.update(id_set)
300
new_paths = [(fp.get_path(t), t) for t in new_ids]
304
def tree_kind(self, trans_id):
305
"""Determine the file kind in the working tree.
307
Raises NoSuchFile if the file does not exist
309
path = self._tree_id_paths.get(trans_id)
311
raise NoSuchFile(None)
313
return file_kind(self._tree.abspath(path))
315
if e.errno != errno.ENOENT:
318
raise NoSuchFile(path)
320
def final_kind(self, trans_id):
321
"""Determine the final file kind, after any changes applied.
323
Raises NoSuchFile if the file does not exist/has no contents.
324
(It is conceivable that a path would be created without the
325
corresponding contents insertion command)
327
if trans_id in self._new_contents:
328
return self._new_contents[trans_id]
329
elif trans_id in self._removed_contents:
330
raise NoSuchFile(None)
332
return self.tree_kind(trans_id)
334
def get_tree_file_id(self, trans_id):
335
"""Determine the file id associated with the trans_id in the tree"""
337
path = self._tree_id_paths[trans_id]
339
# the file is a new, unversioned file, or invalid trans_id
341
# the file is old; the old id is still valid
342
if self._new_root == trans_id:
343
return self._tree.inventory.root.file_id
344
return self._tree.inventory.path2id(path)
346
def final_file_id(self, trans_id):
347
"""Determine the file id after any changes are applied, or None.
349
None indicates that the file will not be versioned after changes are
353
# there is a new id for this file
354
assert self._new_id[trans_id] is not None
355
return self._new_id[trans_id]
357
if trans_id in self._removed_id:
359
return self.get_tree_file_id(trans_id)
362
def inactive_file_id(self, trans_id):
363
"""Return the inactive file_id associated with a transaction id.
364
That is, the one in the tree or in non_present_ids.
365
The file_id may actually be active, too.
367
file_id = self.get_tree_file_id(trans_id)
368
if file_id is not None:
370
for key, value in self._non_present_ids.iteritems():
371
if value == trans_id:
375
def final_parent(self, trans_id):
376
"""Determine the parent file_id, after any changes are applied.
378
ROOT_PARENT is returned for the tree root.
381
return self._new_parent[trans_id]
383
return self.get_tree_parent(trans_id)
385
def final_name(self, trans_id):
386
"""Determine the final filename, after all changes are applied."""
388
return self._new_name[trans_id]
390
return os.path.basename(self._tree_id_paths[trans_id])
392
def _by_parent(self):
393
"""Return a map of parent: children for known parents.
395
Only new paths and parents of tree files with assigned ids are used.
398
items = list(self._new_parent.iteritems())
399
items.extend((t, self.final_parent(t)) for t in
400
self._tree_id_paths.keys())
401
for trans_id, parent_id in items:
402
if parent_id not in by_parent:
403
by_parent[parent_id] = set()
404
by_parent[parent_id].add(trans_id)
407
def path_changed(self, trans_id):
408
"""Return True if a trans_id's path has changed."""
409
return trans_id in self._new_name or trans_id in self._new_parent
411
def find_conflicts(self):
412
"""Find any violations of inventory or filesystem invariants"""
413
if self.__done is True:
414
raise ReusingTransform()
416
# ensure all children of all existent parents are known
417
# all children of non-existent parents are known, by definition.
418
self._add_tree_children()
419
by_parent = self._by_parent()
420
conflicts.extend(self._unversioned_parents(by_parent))
421
conflicts.extend(self._parent_loops())
422
conflicts.extend(self._duplicate_entries(by_parent))
423
conflicts.extend(self._duplicate_ids())
424
conflicts.extend(self._parent_type_conflicts(by_parent))
425
conflicts.extend(self._improper_versioning())
426
conflicts.extend(self._executability_conflicts())
427
conflicts.extend(self._overwrite_conflicts())
430
def _add_tree_children(self):
431
"""Add all the children of all active parents to the known paths.
433
Active parents are those which gain children, and those which are
434
removed. This is a necessary first step in detecting conflicts.
436
parents = self._by_parent().keys()
437
parents.extend([t for t in self._removed_contents if
438
self.tree_kind(t) == 'directory'])
439
for trans_id in self._removed_id:
440
file_id = self.get_tree_file_id(trans_id)
441
if self._tree.inventory[file_id].kind in ('directory',
443
parents.append(trans_id)
445
for parent_id in parents:
446
# ensure that all children are registered with the transaction
447
list(self.iter_tree_children(parent_id))
449
def iter_tree_children(self, parent_id):
450
"""Iterate through the entry's tree children, if any"""
452
path = self._tree_id_paths[parent_id]
456
children = os.listdir(self._tree.abspath(path))
458
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
462
for child in children:
463
childpath = joinpath(path, child)
464
if childpath == BZRDIR:
466
yield self.get_tree_path_id(childpath)
468
def _parent_loops(self):
469
"""No entry should be its own ancestor"""
471
for trans_id in self._new_parent:
474
while parent_id is not ROOT_PARENT:
476
parent_id = self.final_parent(parent_id)
477
if parent_id == trans_id:
478
conflicts.append(('parent loop', trans_id))
479
if parent_id in seen:
483
def _unversioned_parents(self, by_parent):
484
"""If parent directories are versioned, children must be versioned."""
486
for parent_id, children in by_parent.iteritems():
487
if parent_id is ROOT_PARENT:
489
if self.final_file_id(parent_id) is not None:
491
for child_id in children:
492
if self.final_file_id(child_id) is not None:
493
conflicts.append(('unversioned parent', parent_id))
497
def _improper_versioning(self):
498
"""Cannot version a file with no contents, or a bad type.
500
However, existing entries with no contents are okay.
503
for trans_id in self._new_id.iterkeys():
505
kind = self.final_kind(trans_id)
507
conflicts.append(('versioning no contents', trans_id))
509
if not InventoryEntry.versionable_kind(kind):
510
conflicts.append(('versioning bad kind', trans_id, kind))
513
def _executability_conflicts(self):
514
"""Check for bad executability changes.
516
Only versioned files may have their executability set, because
517
1. only versioned entries can have executability under windows
518
2. only files can be executable. (The execute bit on a directory
519
does not indicate searchability)
522
for trans_id in self._new_executability:
523
if self.final_file_id(trans_id) is None:
524
conflicts.append(('unversioned executability', trans_id))
527
non_file = self.final_kind(trans_id) != "file"
531
conflicts.append(('non-file executability', trans_id))
534
def _overwrite_conflicts(self):
535
"""Check for overwrites (not permitted on Win32)"""
537
for trans_id in self._new_contents:
539
self.tree_kind(trans_id)
542
if trans_id not in self._removed_contents:
543
conflicts.append(('overwrite', trans_id,
544
self.final_name(trans_id)))
547
def _duplicate_entries(self, by_parent):
548
"""No directory may have two entries with the same name."""
550
for children in by_parent.itervalues():
551
name_ids = [(self.final_name(t), t) for t in children]
555
for name, trans_id in name_ids:
556
if name == last_name:
557
conflicts.append(('duplicate', last_trans_id, trans_id,
560
last_trans_id = trans_id
563
def _duplicate_ids(self):
564
"""Each inventory id may only be used once"""
566
removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
568
active_tree_ids = set((f for f in self._tree.inventory if
569
f not in removed_tree_ids))
570
for trans_id, file_id in self._new_id.iteritems():
571
if file_id in active_tree_ids:
572
old_trans_id = self.get_id_tree(file_id)
573
conflicts.append(('duplicate id', old_trans_id, trans_id))
576
def _parent_type_conflicts(self, by_parent):
577
"""parents must have directory 'contents'."""
579
for parent_id, children in by_parent.iteritems():
580
if parent_id is ROOT_PARENT:
582
if not self._any_contents(children):
584
for child in children:
586
self.final_kind(child)
590
kind = self.final_kind(parent_id)
594
conflicts.append(('missing parent', parent_id))
595
elif kind != "directory":
596
conflicts.append(('non-directory parent', parent_id))
599
def _any_contents(self, trans_ids):
600
"""Return true if any of the trans_ids, will have contents."""
601
for trans_id in trans_ids:
603
kind = self.final_kind(trans_id)
610
"""Apply all changes to the inventory and filesystem.
612
If filesystem or inventory conflicts are present, MalformedTransform
615
conflicts = self.find_conflicts()
616
if len(conflicts) != 0:
617
raise MalformedTransform(conflicts=conflicts)
619
inv = self._tree.inventory
620
self._apply_removals(inv, limbo_inv)
621
self._apply_insertions(inv, limbo_inv)
622
self._tree._write_inventory(inv)
626
def _limbo_name(self, trans_id):
627
"""Generate the limbo name of a file"""
628
return os.path.join(self._limbodir, trans_id)
630
def _apply_removals(self, inv, limbo_inv):
631
"""Perform tree operations that remove directory/inventory names.
633
That is, delete files that are to be deleted, and put any files that
634
need renaming into limbo. This must be done in strict child-to-parent
637
tree_paths = list(self._tree_path_ids.iteritems())
638
tree_paths.sort(reverse=True)
639
for path, trans_id in tree_paths:
640
full_path = self._tree.abspath(path)
641
if trans_id in self._removed_contents:
642
self.delete_any(full_path)
643
elif trans_id in self._new_name or trans_id in self._new_parent:
645
os.rename(full_path, self._limbo_name(trans_id))
647
if e.errno != errno.ENOENT:
649
if trans_id in self._removed_id:
650
if trans_id == self._new_root:
651
file_id = self._tree.inventory.root.file_id
653
file_id = self.get_tree_file_id(trans_id)
655
elif trans_id in self._new_name or trans_id in self._new_parent:
656
file_id = self.get_tree_file_id(trans_id)
657
if file_id is not None:
658
limbo_inv[trans_id] = inv[file_id]
661
def _apply_insertions(self, inv, limbo_inv):
662
"""Perform tree operations that insert directory/inventory names.
664
That is, create any files that need to be created, and restore from
665
limbo any files that needed renaming. This must be done in strict
666
parent-to-child order.
668
for path, trans_id in self.new_paths():
670
kind = self._new_contents[trans_id]
672
kind = contents = None
673
if trans_id in self._new_contents or self.path_changed(trans_id):
674
full_path = self._tree.abspath(path)
676
os.rename(self._limbo_name(trans_id), full_path)
678
# We may be renaming a dangling inventory id
679
if e.errno != errno.ENOENT:
681
if trans_id in self._new_contents:
682
del self._new_contents[trans_id]
684
if trans_id in self._new_id:
686
kind = file_kind(self._tree.abspath(path))
687
inv.add_path(path, kind, self._new_id[trans_id])
688
elif trans_id in self._new_name or trans_id in self._new_parent:
689
entry = limbo_inv.get(trans_id)
690
if entry is not None:
691
entry.name = self.final_name(trans_id)
692
parent_path = os.path.dirname(path)
693
entry.parent_id = self._tree.inventory.path2id(parent_path)
696
# requires files and inventory entries to be in place
697
if trans_id in self._new_executability:
698
self._set_executability(path, inv, trans_id)
700
def _set_executability(self, path, inv, trans_id):
701
"""Set the executability of versioned files """
702
file_id = inv.path2id(path)
703
new_executability = self._new_executability[trans_id]
704
inv[file_id].executable = new_executability
705
if supports_executable():
706
abspath = self._tree.abspath(path)
707
current_mode = os.stat(abspath).st_mode
708
if new_executability:
711
to_mode = current_mode | (0100 & ~umask)
712
# Enable x-bit for others only if they can read it.
713
if current_mode & 0004:
714
to_mode |= 0001 & ~umask
715
if current_mode & 0040:
716
to_mode |= 0010 & ~umask
718
to_mode = current_mode & ~0111
719
os.chmod(abspath, to_mode)
721
def _new_entry(self, name, parent_id, file_id):
722
"""Helper function to create a new filesystem entry."""
723
trans_id = self.create_path(name, parent_id)
724
if file_id is not None:
725
self.version_file(file_id, trans_id)
728
def new_file(self, name, parent_id, contents, file_id=None,
730
"""Convenience method to create files.
732
name is the name of the file to create.
733
parent_id is the transaction id of the parent directory of the file.
734
contents is an iterator of bytestrings, which will be used to produce
736
file_id is the inventory ID of the file, if it is to be versioned.
738
trans_id = self._new_entry(name, parent_id, file_id)
739
self.create_file(contents, trans_id)
740
if executable is not None:
741
self.set_executability(executable, trans_id)
744
def new_directory(self, name, parent_id, file_id=None):
745
"""Convenience method to create directories.
747
name is the name of the directory to create.
748
parent_id is the transaction id of the parent directory of the
750
file_id is the inventory ID of the directory, if it is to be versioned.
752
trans_id = self._new_entry(name, parent_id, file_id)
753
self.create_directory(trans_id)
756
def new_symlink(self, name, parent_id, target, file_id=None):
757
"""Convenience method to create symbolic link.
759
name is the name of the symlink to create.
760
parent_id is the transaction id of the parent directory of the symlink.
761
target is a bytestring of the target of the symlink.
762
file_id is the inventory ID of the file, if it is to be versioned.
764
trans_id = self._new_entry(name, parent_id, file_id)
765
self.create_symlink(target, trans_id)
768
def joinpath(parent, child):
769
"""Join tree-relative paths, handling the tree root specially"""
770
if parent is None or parent == "":
773
return os.path.join(parent, child)
775
class FinalPaths(object):
776
"""Make path calculation cheap by memoizing paths.
778
The underlying tree must not be manipulated between calls, or else
779
the results will likely be incorrect.
781
def __init__(self, transform):
782
object.__init__(self)
783
self._known_paths = {}
784
self.transform = transform
786
def _determine_path(self, trans_id):
787
if trans_id == self.transform.root:
789
name = self.transform.final_name(trans_id)
790
parent_id = self.transform.final_parent(trans_id)
791
if parent_id == self.transform.root:
794
return os.path.join(self.get_path(parent_id), name)
796
def get_path(self, trans_id):
797
"""Find the final path associated with a trans_id"""
798
if trans_id not in self._known_paths:
799
self._known_paths[trans_id] = self._determine_path(trans_id)
800
return self._known_paths[trans_id]
802
def topology_sorted_ids(tree):
803
"""Determine the topological order of the ids in a tree"""
804
file_ids = list(tree)
805
file_ids.sort(key=tree.id2path)
808
def build_tree(branch, tree):
809
"""Create working tree for a branch, using a Transaction."""
811
wt = branch.working_tree()
812
tt = TreeTransform(wt)
814
file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
815
file_ids = topology_sorted_ids(tree)
816
for file_id in file_ids:
817
entry = tree.inventory[file_id]
818
if entry.parent_id is None:
820
if entry.parent_id not in file_trans_id:
821
raise repr(entry.parent_id)
822
parent_id = file_trans_id[entry.parent_id]
823
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
828
def new_by_entry(tt, entry, parent_id, tree):
829
"""Create a new file according to its inventory entry"""
833
contents = tree.get_file(entry.file_id).readlines()
834
executable = tree.is_executable(entry.file_id)
835
return tt.new_file(name, parent_id, contents, entry.file_id,
837
elif kind == 'directory':
838
return tt.new_directory(name, parent_id, entry.file_id)
839
elif kind == 'symlink':
840
target = entry.get_symlink_target(file_id)
841
return tt.new_symlink(name, parent_id, target, file_id)
843
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
844
"""Create new file contents according to an inventory entry."""
845
if entry.kind == "file":
847
lines = tree.get_file(entry.file_id).readlines()
848
tt.create_file(lines, trans_id, mode_id=mode_id)
849
elif entry.kind == "symlink":
850
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
851
elif entry.kind == "directory":
852
tt.create_directory(trans_id)
854
def create_entry_executability(tt, entry, trans_id):
855
"""Set the executability of a trans_id according to an inventory entry"""
856
if entry.kind == "file":
857
tt.set_executability(entry.executable, trans_id)
860
def find_interesting(working_tree, target_tree, filenames):
861
"""Find the ids corresponding to specified filenames."""
863
interesting_ids = None
865
interesting_ids = set()
866
for tree_path in filenames:
867
for tree in (working_tree, target_tree):
869
file_id = tree.inventory.path2id(tree_path)
870
if file_id is not None:
871
interesting_ids.add(file_id)
874
raise NotVersionedError(path=tree_path)
875
return interesting_ids
878
def change_entry(tt, file_id, working_tree, target_tree,
879
get_trans_id, backups, trans_id):
880
"""Replace a file_id's contents with those from a target tree."""
881
e_trans_id = get_trans_id(file_id)
882
entry = target_tree.inventory[file_id]
883
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
889
tt.delete_contents(e_trans_id)
891
parent_trans_id = get_trans_id(entry.parent_id)
892
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
893
tt.unversion_file(e_trans_id)
894
e_trans_id = tt.create_path(entry.name, parent_trans_id)
895
tt.version_file(file_id, e_trans_id)
896
trans_id[file_id] = e_trans_id
897
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
898
create_entry_executability(tt, entry, e_trans_id)
901
tt.set_executability(entry.executable, e_trans_id)
902
if tt.final_name(e_trans_id) != entry.name:
905
parent_id = tt.final_parent(e_trans_id)
906
parent_file_id = tt.final_file_id(parent_id)
907
if parent_file_id != entry.parent_id:
912
parent_trans_id = get_trans_id(entry.parent_id)
913
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
916
def _entry_changes(file_id, entry, working_tree):
917
"""Determine in which ways the inventory entry has changed.
919
Returns booleans: has_contents, content_mod, meta_mod
920
has_contents means there are currently contents, but they differ
921
contents_mod means contents need to be modified
922
meta_mod means the metadata needs to be modified
924
cur_entry = working_tree.inventory[file_id]
926
working_kind = working_tree.kind(file_id)
929
if e.errno != errno.ENOENT:
934
if has_contents is True:
935
real_e_kind = entry.kind
936
if real_e_kind == 'root_directory':
937
real_e_kind = 'directory'
938
if real_e_kind != working_kind:
939
contents_mod, meta_mod = True, False
941
cur_entry._read_tree_state(working_tree.id2path(file_id),
943
contents_mod, meta_mod = entry.detect_changes(cur_entry)
944
return has_contents, contents_mod, meta_mod
947
def revert(working_tree, target_tree, filenames, backups=False):
948
"""Revert a working tree's contents to those of a target tree."""
949
interesting_ids = find_interesting(working_tree, target_tree, filenames)
950
def interesting(file_id):
951
return interesting_ids is None or file_id in interesting_ids
953
tt = TreeTransform(working_tree)
956
def get_trans_id(file_id):
958
return trans_id[file_id]
960
return tt.get_id_tree(file_id)
962
for file_id in topology_sorted_ids(target_tree):
963
if not interesting(file_id):
965
if file_id not in working_tree.inventory:
966
entry = target_tree.inventory[file_id]
967
parent_id = get_trans_id(entry.parent_id)
968
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
969
trans_id[file_id] = e_trans_id
971
change_entry(tt, file_id, working_tree, target_tree,
972
get_trans_id, backups, trans_id)
973
for file_id in working_tree:
974
if not interesting(file_id):
976
if file_id not in target_tree:
977
tt.unversion_file(tt.get_id_tree(file_id))
978
resolve_conflicts(tt)
984
def resolve_conflicts(tt):
985
"""Make many conflict-resolution attempts, but die if they fail"""
987
conflicts = tt.find_conflicts()
988
if len(conflicts) == 0:
990
conflict_pass(tt, conflicts)
991
raise MalformedTransform(conflicts=conflicts)
994
def conflict_pass(tt, conflicts):
995
"""Resolve some classes of conflicts."""
996
for c_type, conflict in ((c[0], c) for c in conflicts):
997
if c_type == 'duplicate id':
998
tt.unversion_file(conflict[1])
999
elif c_type == 'duplicate':
1000
# files that were renamed take precedence
1001
new_name = tt.final_name(conflict[1])+'.moved'
1002
final_parent = tt.final_parent(conflict[1])
1003
if tt.path_changed(conflict[1]):
1004
tt.adjust_path(new_name, final_parent, conflict[2])
1006
tt.adjust_path(new_name, final_parent, conflict[1])
1007
elif c_type == 'parent loop':
1008
# break the loop by undoing one of the ops that caused the loop
1010
while not tt.path_changed(cur):
1011
cur = tt.final_parent(cur)
1012
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1013
elif c_type == 'missing parent':
1014
trans_id = conflict[1]
1016
tt.cancel_deletion(trans_id)
1018
tt.create_directory(trans_id)
1019
elif c_type == 'unversioned parent':
1020
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])