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.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
22
ReusingTransform, NotVersionedError, CantMoveRoot,
23
ExistingLimbo, ImmortalLimbo)
24
from bzrlib.inventory import InventoryEntry
25
from bzrlib.osutils import file_kind, supports_executable, pathjoin
26
from bzrlib.trace import mutter, warning
29
ROOT_PARENT = "root-parent"
32
def unique_add(map, key, value):
34
raise DuplicateKey(key=key)
38
class TreeTransform(object):
39
"""Represent a tree transformation.
41
This object is designed to support incremental generation of the transform,
44
It is easy to produce malformed transforms, but they are generally
45
harmless. Attempting to apply a malformed transform will cause an
46
exception to be raised before any modifications are made to the tree.
48
Many kinds of malformed transforms can be corrected with the
49
resolve_conflicts function. The remaining ones indicate programming error,
50
such as trying to create a file with no path.
52
Two sets of file creation methods are supplied. Convenience methods are:
57
These are composed of the low-level methods:
59
* create_file or create_directory or create_symlink
63
def __init__(self, tree):
64
"""Note: a write lock is taken on the tree.
66
Use TreeTransform.finalize() to release the lock
70
self._tree.lock_write()
72
control_files = self._tree._control_files
73
self._limbodir = control_files.controlfilename('limbo')
75
os.mkdir(self._limbodir)
77
if e.errno == errno.EEXIST:
78
raise ExistingLimbo(self._limbodir)
86
self._new_contents = {}
87
self._removed_contents = set()
88
self._new_executability = {}
90
self._non_present_ids = {}
92
self._removed_id = set()
93
self._tree_path_ids = {}
94
self._tree_id_paths = {}
95
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
101
root = property(__get_root)
104
"""Release the working tree lock, if held, clean up limbo dir."""
105
if self._tree is None:
108
for trans_id, kind in self._new_contents.iteritems():
109
path = self._limbo_name(trans_id)
110
if kind == "directory":
115
os.rmdir(self._limbodir)
117
# We don't especially care *why* the dir is immortal.
118
raise ImmortalLimbo(self._limbodir)
123
def _assign_id(self):
124
"""Produce a new tranform id"""
125
new_id = "new-%s" % self._id_number
129
def create_path(self, name, parent):
130
"""Assign a transaction id to a new path"""
131
trans_id = self._assign_id()
132
unique_add(self._new_name, trans_id, name)
133
unique_add(self._new_parent, trans_id, parent)
136
def adjust_path(self, name, parent, trans_id):
137
"""Change the path that is assigned to a transaction id."""
138
if trans_id == self._new_root:
140
self._new_name[trans_id] = name
141
self._new_parent[trans_id] = parent
143
def adjust_root_path(self, name, parent):
144
"""Emulate moving the root by moving all children, instead.
146
We do this by undoing the association of root's transaction id with the
147
current tree. This allows us to create a new directory with that
148
transaction id. We unversion the root directory and version the
149
physically new directory, and hope someone versions the tree root
152
old_root = self._new_root
153
old_root_file_id = self.final_file_id(old_root)
154
# force moving all children of root
155
for child_id in self.iter_tree_children(old_root):
156
if child_id != parent:
157
self.adjust_path(self.final_name(child_id),
158
self.final_parent(child_id), child_id)
159
file_id = self.final_file_id(child_id)
160
if file_id is not None:
161
self.unversion_file(child_id)
162
self.version_file(file_id, child_id)
164
# the physical root needs a new transaction id
165
self._tree_path_ids.pop("")
166
self._tree_id_paths.pop(old_root)
167
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
168
if parent == old_root:
169
parent = self._new_root
170
self.adjust_path(name, parent, old_root)
171
self.create_directory(old_root)
172
self.version_file(old_root_file_id, old_root)
173
self.unversion_file(self._new_root)
175
def trans_id_tree_file_id(self, inventory_id):
176
"""Determine the transaction id of a working tree file.
178
This reflects only files that already exist, not ones that will be
179
added by transactions.
181
path = self._tree.inventory.id2path(inventory_id)
182
return self.trans_id_tree_path(path)
184
def trans_id_file_id(self, file_id):
185
"""Determine or set the transaction id associated with a file ID.
186
A new id is only created for file_ids that were never present. If
187
a transaction has been unversioned, it is deliberately still returned.
188
(this will likely lead to an unversioned parent conflict.)
190
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
191
return self._r_new_id[file_id]
192
elif file_id in self._tree.inventory:
193
return self.trans_id_tree_file_id(file_id)
194
elif file_id in self._non_present_ids:
195
return self._non_present_ids[file_id]
197
trans_id = self._assign_id()
198
self._non_present_ids[file_id] = trans_id
201
def canonical_path(self, path):
202
"""Get the canonical tree-relative path"""
203
# don't follow final symlinks
204
dirname, basename = os.path.split(self._tree.abspath(path))
205
dirname = os.path.realpath(dirname)
206
return self._tree.relpath(pathjoin(dirname, basename))
208
def trans_id_tree_path(self, path):
209
"""Determine (and maybe set) the transaction ID for a tree path."""
210
path = self.canonical_path(path)
211
if path not in self._tree_path_ids:
212
self._tree_path_ids[path] = self._assign_id()
213
self._tree_id_paths[self._tree_path_ids[path]] = path
214
return self._tree_path_ids[path]
216
def get_tree_parent(self, trans_id):
217
"""Determine id of the parent in the tree."""
218
path = self._tree_id_paths[trans_id]
221
return self.trans_id_tree_path(os.path.dirname(path))
223
def create_file(self, contents, trans_id, mode_id=None):
224
"""Schedule creation of a new file.
228
Contents is an iterator of strings, all of which will be written
229
to the target destination.
231
New file takes the permissions of any existing file with that id,
232
unless mode_id is specified.
234
f = file(self._limbo_name(trans_id), 'wb')
235
unique_add(self._new_contents, trans_id, 'file')
236
for segment in contents:
239
self._set_mode(trans_id, mode_id, S_ISREG)
241
def _set_mode(self, trans_id, mode_id, typefunc):
242
"""Set the mode of new file contents.
243
The mode_id is the existing file to get the mode from (often the same
244
as trans_id). The operation is only performed if there's a mode match
245
according to typefunc.
250
old_path = self._tree_id_paths[mode_id]
254
mode = os.stat(old_path).st_mode
256
if e.errno == errno.ENOENT:
261
os.chmod(self._limbo_name(trans_id), mode)
263
def create_directory(self, trans_id):
264
"""Schedule creation of a new directory.
266
See also new_directory.
268
os.mkdir(self._limbo_name(trans_id))
269
unique_add(self._new_contents, trans_id, 'directory')
271
def create_symlink(self, target, trans_id):
272
"""Schedule creation of a new symbolic link.
274
target is a bytestring.
275
See also new_symlink.
277
os.symlink(target, self._limbo_name(trans_id))
278
unique_add(self._new_contents, trans_id, 'symlink')
281
def delete_any(full_path):
282
"""Delete a file or directory."""
286
# We may be renaming a dangling inventory id
287
if e.errno != errno.EISDIR and e.errno != errno.EACCES:
291
def cancel_creation(self, trans_id):
292
"""Cancel the creation of new file contents."""
293
del self._new_contents[trans_id]
294
self.delete_any(self._limbo_name(trans_id))
296
def delete_contents(self, trans_id):
297
"""Schedule the contents of a path entry for deletion"""
298
self.tree_kind(trans_id)
299
self._removed_contents.add(trans_id)
301
def cancel_deletion(self, trans_id):
302
"""Cancel a scheduled deletion"""
303
self._removed_contents.remove(trans_id)
305
def unversion_file(self, trans_id):
306
"""Schedule a path entry to become unversioned"""
307
self._removed_id.add(trans_id)
309
def delete_versioned(self, trans_id):
310
"""Delete and unversion a versioned file"""
311
self.delete_contents(trans_id)
312
self.unversion_file(trans_id)
314
def set_executability(self, executability, trans_id):
315
"""Schedule setting of the 'execute' bit
316
To unschedule, set to None
318
if executability is None:
319
del self._new_executability[trans_id]
321
unique_add(self._new_executability, trans_id, executability)
323
def version_file(self, file_id, trans_id):
324
"""Schedule a file to become versioned."""
325
assert file_id is not None
326
unique_add(self._new_id, trans_id, file_id)
327
unique_add(self._r_new_id, file_id, trans_id)
329
def cancel_versioning(self, trans_id):
330
"""Undo a previous versioning of a file"""
331
file_id = self._new_id[trans_id]
332
del self._new_id[trans_id]
333
del self._r_new_id[file_id]
336
"""Determine the paths of all new and changed files"""
338
fp = FinalPaths(self)
339
for id_set in (self._new_name, self._new_parent, self._new_contents,
340
self._new_id, self._new_executability):
341
new_ids.update(id_set)
342
new_paths = [(fp.get_path(t), t) for t in new_ids]
346
def tree_kind(self, trans_id):
347
"""Determine the file kind in the working tree.
349
Raises NoSuchFile if the file does not exist
351
path = self._tree_id_paths.get(trans_id)
353
raise NoSuchFile(None)
355
return file_kind(self._tree.abspath(path))
357
if e.errno != errno.ENOENT:
360
raise NoSuchFile(path)
362
def final_kind(self, trans_id):
363
"""Determine the final file kind, after any changes applied.
365
Raises NoSuchFile if the file does not exist/has no contents.
366
(It is conceivable that a path would be created without the
367
corresponding contents insertion command)
369
if trans_id in self._new_contents:
370
return self._new_contents[trans_id]
371
elif trans_id in self._removed_contents:
372
raise NoSuchFile(None)
374
return self.tree_kind(trans_id)
376
def tree_file_id(self, trans_id):
377
"""Determine the file id associated with the trans_id in the tree"""
379
path = self._tree_id_paths[trans_id]
381
# the file is a new, unversioned file, or invalid trans_id
383
# the file is old; the old id is still valid
384
if self._new_root == trans_id:
385
return self._tree.inventory.root.file_id
386
return self._tree.inventory.path2id(path)
388
def final_file_id(self, trans_id):
389
"""Determine the file id after any changes are applied, or None.
391
None indicates that the file will not be versioned after changes are
395
# there is a new id for this file
396
assert self._new_id[trans_id] is not None
397
return self._new_id[trans_id]
399
if trans_id in self._removed_id:
401
return self.tree_file_id(trans_id)
403
def inactive_file_id(self, trans_id):
404
"""Return the inactive file_id associated with a transaction id.
405
That is, the one in the tree or in non_present_ids.
406
The file_id may actually be active, too.
408
file_id = self.tree_file_id(trans_id)
409
if file_id is not None:
411
for key, value in self._non_present_ids.iteritems():
412
if value == trans_id:
415
def final_parent(self, trans_id):
416
"""Determine the parent file_id, after any changes are applied.
418
ROOT_PARENT is returned for the tree root.
421
return self._new_parent[trans_id]
423
return self.get_tree_parent(trans_id)
425
def final_name(self, trans_id):
426
"""Determine the final filename, after all changes are applied."""
428
return self._new_name[trans_id]
430
return os.path.basename(self._tree_id_paths[trans_id])
432
def _by_parent(self):
433
"""Return a map of parent: children for known parents.
435
Only new paths and parents of tree files with assigned ids are used.
438
items = list(self._new_parent.iteritems())
439
items.extend((t, self.final_parent(t)) for t in
440
self._tree_id_paths.keys())
441
for trans_id, parent_id in items:
442
if parent_id not in by_parent:
443
by_parent[parent_id] = set()
444
by_parent[parent_id].add(trans_id)
447
def path_changed(self, trans_id):
448
"""Return True if a trans_id's path has changed."""
449
return trans_id in self._new_name or trans_id in self._new_parent
451
def find_conflicts(self):
452
"""Find any violations of inventory or filesystem invariants"""
453
if self.__done is True:
454
raise ReusingTransform()
456
# ensure all children of all existent parents are known
457
# all children of non-existent parents are known, by definition.
458
self._add_tree_children()
459
by_parent = self._by_parent()
460
conflicts.extend(self._unversioned_parents(by_parent))
461
conflicts.extend(self._parent_loops())
462
conflicts.extend(self._duplicate_entries(by_parent))
463
conflicts.extend(self._duplicate_ids())
464
conflicts.extend(self._parent_type_conflicts(by_parent))
465
conflicts.extend(self._improper_versioning())
466
conflicts.extend(self._executability_conflicts())
467
conflicts.extend(self._overwrite_conflicts())
470
def _add_tree_children(self):
471
"""Add all the children of all active parents to the known paths.
473
Active parents are those which gain children, and those which are
474
removed. This is a necessary first step in detecting conflicts.
476
parents = self._by_parent().keys()
477
parents.extend([t for t in self._removed_contents if
478
self.tree_kind(t) == 'directory'])
479
for trans_id in self._removed_id:
480
file_id = self.tree_file_id(trans_id)
481
if self._tree.inventory[file_id].kind in ('directory',
483
parents.append(trans_id)
485
for parent_id in parents:
486
# ensure that all children are registered with the transaction
487
list(self.iter_tree_children(parent_id))
489
def iter_tree_children(self, parent_id):
490
"""Iterate through the entry's tree children, if any"""
492
path = self._tree_id_paths[parent_id]
496
children = os.listdir(self._tree.abspath(path))
498
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
502
for child in children:
503
childpath = joinpath(path, child)
504
if self._tree.is_control_filename(childpath):
506
yield self.trans_id_tree_path(childpath)
508
def _parent_loops(self):
509
"""No entry should be its own ancestor"""
511
for trans_id in self._new_parent:
514
while parent_id is not ROOT_PARENT:
516
parent_id = self.final_parent(parent_id)
517
if parent_id == trans_id:
518
conflicts.append(('parent loop', trans_id))
519
if parent_id in seen:
523
def _unversioned_parents(self, by_parent):
524
"""If parent directories are versioned, children must be versioned."""
526
for parent_id, children in by_parent.iteritems():
527
if parent_id is ROOT_PARENT:
529
if self.final_file_id(parent_id) is not None:
531
for child_id in children:
532
if self.final_file_id(child_id) is not None:
533
conflicts.append(('unversioned parent', parent_id))
537
def _improper_versioning(self):
538
"""Cannot version a file with no contents, or a bad type.
540
However, existing entries with no contents are okay.
543
for trans_id in self._new_id.iterkeys():
545
kind = self.final_kind(trans_id)
547
conflicts.append(('versioning no contents', trans_id))
549
if not InventoryEntry.versionable_kind(kind):
550
conflicts.append(('versioning bad kind', trans_id, kind))
553
def _executability_conflicts(self):
554
"""Check for bad executability changes.
556
Only versioned files may have their executability set, because
557
1. only versioned entries can have executability under windows
558
2. only files can be executable. (The execute bit on a directory
559
does not indicate searchability)
562
for trans_id in self._new_executability:
563
if self.final_file_id(trans_id) is None:
564
conflicts.append(('unversioned executability', trans_id))
567
non_file = self.final_kind(trans_id) != "file"
571
conflicts.append(('non-file executability', trans_id))
574
def _overwrite_conflicts(self):
575
"""Check for overwrites (not permitted on Win32)"""
577
for trans_id in self._new_contents:
579
self.tree_kind(trans_id)
582
if trans_id not in self._removed_contents:
583
conflicts.append(('overwrite', trans_id,
584
self.final_name(trans_id)))
587
def _duplicate_entries(self, by_parent):
588
"""No directory may have two entries with the same name."""
590
for children in by_parent.itervalues():
591
name_ids = [(self.final_name(t), t) for t in children]
595
for name, trans_id in name_ids:
596
if name == last_name:
597
conflicts.append(('duplicate', last_trans_id, trans_id,
600
last_trans_id = trans_id
603
def _duplicate_ids(self):
604
"""Each inventory id may only be used once"""
606
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
608
active_tree_ids = set((f for f in self._tree.inventory if
609
f not in removed_tree_ids))
610
for trans_id, file_id in self._new_id.iteritems():
611
if file_id in active_tree_ids:
612
old_trans_id = self.trans_id_tree_file_id(file_id)
613
conflicts.append(('duplicate id', old_trans_id, trans_id))
616
def _parent_type_conflicts(self, by_parent):
617
"""parents must have directory 'contents'."""
619
for parent_id, children in by_parent.iteritems():
620
if parent_id is ROOT_PARENT:
622
if not self._any_contents(children):
624
for child in children:
626
self.final_kind(child)
630
kind = self.final_kind(parent_id)
634
conflicts.append(('missing parent', parent_id))
635
elif kind != "directory":
636
conflicts.append(('non-directory parent', parent_id))
639
def _any_contents(self, trans_ids):
640
"""Return true if any of the trans_ids, will have contents."""
641
for trans_id in trans_ids:
643
kind = self.final_kind(trans_id)
650
"""Apply all changes to the inventory and filesystem.
652
If filesystem or inventory conflicts are present, MalformedTransform
655
conflicts = self.find_conflicts()
656
if len(conflicts) != 0:
657
raise MalformedTransform(conflicts=conflicts)
659
inv = self._tree.inventory
660
self._apply_removals(inv, limbo_inv)
661
self._apply_insertions(inv, limbo_inv)
662
self._tree._write_inventory(inv)
666
def _limbo_name(self, trans_id):
667
"""Generate the limbo name of a file"""
668
return pathjoin(self._limbodir, trans_id)
670
def _apply_removals(self, inv, limbo_inv):
671
"""Perform tree operations that remove directory/inventory names.
673
That is, delete files that are to be deleted, and put any files that
674
need renaming into limbo. This must be done in strict child-to-parent
677
tree_paths = list(self._tree_path_ids.iteritems())
678
tree_paths.sort(reverse=True)
679
for path, trans_id in tree_paths:
680
full_path = self._tree.abspath(path)
681
if trans_id in self._removed_contents:
682
self.delete_any(full_path)
683
elif trans_id in self._new_name or trans_id in self._new_parent:
685
os.rename(full_path, self._limbo_name(trans_id))
687
if e.errno != errno.ENOENT:
689
if trans_id in self._removed_id:
690
if trans_id == self._new_root:
691
file_id = self._tree.inventory.root.file_id
693
file_id = self.tree_file_id(trans_id)
695
elif trans_id in self._new_name or trans_id in self._new_parent:
696
file_id = self.tree_file_id(trans_id)
697
if file_id is not None:
698
limbo_inv[trans_id] = inv[file_id]
701
def _apply_insertions(self, inv, limbo_inv):
702
"""Perform tree operations that insert directory/inventory names.
704
That is, create any files that need to be created, and restore from
705
limbo any files that needed renaming. This must be done in strict
706
parent-to-child order.
708
for path, trans_id in self.new_paths():
710
kind = self._new_contents[trans_id]
712
kind = contents = None
713
if trans_id in self._new_contents or self.path_changed(trans_id):
714
full_path = self._tree.abspath(path)
716
os.rename(self._limbo_name(trans_id), full_path)
718
# We may be renaming a dangling inventory id
719
if e.errno != errno.ENOENT:
721
if trans_id in self._new_contents:
722
del self._new_contents[trans_id]
724
if trans_id in self._new_id:
726
kind = file_kind(self._tree.abspath(path))
727
inv.add_path(path, kind, self._new_id[trans_id])
728
elif trans_id in self._new_name or trans_id in self._new_parent:
729
entry = limbo_inv.get(trans_id)
730
if entry is not None:
731
entry.name = self.final_name(trans_id)
732
parent_path = os.path.dirname(path)
733
entry.parent_id = self._tree.inventory.path2id(parent_path)
736
# requires files and inventory entries to be in place
737
if trans_id in self._new_executability:
738
self._set_executability(path, inv, trans_id)
740
def _set_executability(self, path, inv, trans_id):
741
"""Set the executability of versioned files """
742
file_id = inv.path2id(path)
743
new_executability = self._new_executability[trans_id]
744
inv[file_id].executable = new_executability
745
if supports_executable():
746
abspath = self._tree.abspath(path)
747
current_mode = os.stat(abspath).st_mode
748
if new_executability:
751
to_mode = current_mode | (0100 & ~umask)
752
# Enable x-bit for others only if they can read it.
753
if current_mode & 0004:
754
to_mode |= 0001 & ~umask
755
if current_mode & 0040:
756
to_mode |= 0010 & ~umask
758
to_mode = current_mode & ~0111
759
os.chmod(abspath, to_mode)
761
def _new_entry(self, name, parent_id, file_id):
762
"""Helper function to create a new filesystem entry."""
763
trans_id = self.create_path(name, parent_id)
764
if file_id is not None:
765
self.version_file(file_id, trans_id)
768
def new_file(self, name, parent_id, contents, file_id=None,
770
"""Convenience method to create files.
772
name is the name of the file to create.
773
parent_id is the transaction id of the parent directory of the file.
774
contents is an iterator of bytestrings, which will be used to produce
776
file_id is the inventory ID of the file, if it is to be versioned.
778
trans_id = self._new_entry(name, parent_id, file_id)
779
self.create_file(contents, trans_id)
780
if executable is not None:
781
self.set_executability(executable, trans_id)
784
def new_directory(self, name, parent_id, file_id=None):
785
"""Convenience method to create directories.
787
name is the name of the directory to create.
788
parent_id is the transaction id of the parent directory of the
790
file_id is the inventory ID of the directory, if it is to be versioned.
792
trans_id = self._new_entry(name, parent_id, file_id)
793
self.create_directory(trans_id)
796
def new_symlink(self, name, parent_id, target, file_id=None):
797
"""Convenience method to create symbolic link.
799
name is the name of the symlink to create.
800
parent_id is the transaction id of the parent directory of the symlink.
801
target is a bytestring of the target of the symlink.
802
file_id is the inventory ID of the file, if it is to be versioned.
804
trans_id = self._new_entry(name, parent_id, file_id)
805
self.create_symlink(target, trans_id)
808
def joinpath(parent, child):
809
"""Join tree-relative paths, handling the tree root specially"""
810
if parent is None or parent == "":
813
return pathjoin(parent, child)
816
class FinalPaths(object):
817
"""Make path calculation cheap by memoizing paths.
819
The underlying tree must not be manipulated between calls, or else
820
the results will likely be incorrect.
822
def __init__(self, transform):
823
object.__init__(self)
824
self._known_paths = {}
825
self.transform = transform
827
def _determine_path(self, trans_id):
828
if trans_id == self.transform.root:
830
name = self.transform.final_name(trans_id)
831
parent_id = self.transform.final_parent(trans_id)
832
if parent_id == self.transform.root:
835
return pathjoin(self.get_path(parent_id), name)
837
def get_path(self, trans_id):
838
"""Find the final path associated with a trans_id"""
839
if trans_id not in self._known_paths:
840
self._known_paths[trans_id] = self._determine_path(trans_id)
841
return self._known_paths[trans_id]
843
def topology_sorted_ids(tree):
844
"""Determine the topological order of the ids in a tree"""
845
file_ids = list(tree)
846
file_ids.sort(key=tree.id2path)
849
def build_tree(tree, wt):
850
"""Create working tree for a branch, using a Transaction."""
852
tt = TreeTransform(wt)
854
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
855
file_ids = topology_sorted_ids(tree)
856
for file_id in file_ids:
857
entry = tree.inventory[file_id]
858
if entry.parent_id is None:
860
if entry.parent_id not in file_trans_id:
861
raise repr(entry.parent_id)
862
parent_id = file_trans_id[entry.parent_id]
863
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
868
def new_by_entry(tt, entry, parent_id, tree):
869
"""Create a new file according to its inventory entry"""
873
contents = tree.get_file(entry.file_id).readlines()
874
executable = tree.is_executable(entry.file_id)
875
return tt.new_file(name, parent_id, contents, entry.file_id,
877
elif kind == 'directory':
878
return tt.new_directory(name, parent_id, entry.file_id)
879
elif kind == 'symlink':
880
target = tree.get_symlink_target(entry.file_id)
881
return tt.new_symlink(name, parent_id, target, entry.file_id)
883
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
884
"""Create new file contents according to an inventory entry."""
885
if entry.kind == "file":
887
lines = tree.get_file(entry.file_id).readlines()
888
tt.create_file(lines, trans_id, mode_id=mode_id)
889
elif entry.kind == "symlink":
890
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
891
elif entry.kind == "directory":
892
tt.create_directory(trans_id)
894
def create_entry_executability(tt, entry, trans_id):
895
"""Set the executability of a trans_id according to an inventory entry"""
896
if entry.kind == "file":
897
tt.set_executability(entry.executable, trans_id)
900
def find_interesting(working_tree, target_tree, filenames):
901
"""Find the ids corresponding to specified filenames."""
903
interesting_ids = None
905
interesting_ids = set()
906
for tree_path in filenames:
907
for tree in (working_tree, target_tree):
909
file_id = tree.inventory.path2id(tree_path)
910
if file_id is not None:
911
interesting_ids.add(file_id)
914
raise NotVersionedError(path=tree_path)
915
return interesting_ids
918
def change_entry(tt, file_id, working_tree, target_tree,
919
trans_id_file_id, backups, trans_id):
920
"""Replace a file_id's contents with those from a target tree."""
921
e_trans_id = trans_id_file_id(file_id)
922
entry = target_tree.inventory[file_id]
923
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
929
tt.delete_contents(e_trans_id)
931
parent_trans_id = trans_id_file_id(entry.parent_id)
932
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
933
tt.unversion_file(e_trans_id)
934
e_trans_id = tt.create_path(entry.name, parent_trans_id)
935
tt.version_file(file_id, e_trans_id)
936
trans_id[file_id] = e_trans_id
937
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
938
create_entry_executability(tt, entry, e_trans_id)
941
tt.set_executability(entry.executable, e_trans_id)
942
if tt.final_name(e_trans_id) != entry.name:
945
parent_id = tt.final_parent(e_trans_id)
946
parent_file_id = tt.final_file_id(parent_id)
947
if parent_file_id != entry.parent_id:
952
parent_trans_id = trans_id_file_id(entry.parent_id)
953
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
956
def _entry_changes(file_id, entry, working_tree):
957
"""Determine in which ways the inventory entry has changed.
959
Returns booleans: has_contents, content_mod, meta_mod
960
has_contents means there are currently contents, but they differ
961
contents_mod means contents need to be modified
962
meta_mod means the metadata needs to be modified
964
cur_entry = working_tree.inventory[file_id]
966
working_kind = working_tree.kind(file_id)
969
if e.errno != errno.ENOENT:
974
if has_contents is True:
975
real_e_kind = entry.kind
976
if real_e_kind == 'root_directory':
977
real_e_kind = 'directory'
978
if real_e_kind != working_kind:
979
contents_mod, meta_mod = True, False
981
cur_entry._read_tree_state(working_tree.id2path(file_id),
983
contents_mod, meta_mod = entry.detect_changes(cur_entry)
984
cur_entry._forget_tree_state()
985
return has_contents, contents_mod, meta_mod
988
def revert(working_tree, target_tree, filenames, backups=False):
989
"""Revert a working tree's contents to those of a target tree."""
990
interesting_ids = find_interesting(working_tree, target_tree, filenames)
991
def interesting(file_id):
992
return interesting_ids is None or file_id in interesting_ids
994
tt = TreeTransform(working_tree)
997
def trans_id_file_id(file_id):
999
return trans_id[file_id]
1001
return tt.trans_id_tree_file_id(file_id)
1003
for file_id in topology_sorted_ids(target_tree):
1004
if not interesting(file_id):
1006
if file_id not in working_tree.inventory:
1007
entry = target_tree.inventory[file_id]
1008
parent_id = trans_id_file_id(entry.parent_id)
1009
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1010
trans_id[file_id] = e_trans_id
1012
change_entry(tt, file_id, working_tree, target_tree,
1013
trans_id_file_id, backups, trans_id)
1014
for file_id in working_tree.inventory:
1015
if not interesting(file_id):
1017
if file_id not in target_tree:
1018
tt.unversion_file(tt.trans_id_tree_file_id(file_id))
1019
raw_conflicts = resolve_conflicts(tt)
1020
for line in conflicts_strings(cook_conflicts(raw_conflicts, tt)):
1027
def resolve_conflicts(tt):
1028
"""Make many conflict-resolution attempts, but die if they fail"""
1029
new_conflicts = set()
1031
conflicts = tt.find_conflicts()
1032
if len(conflicts) == 0:
1033
return new_conflicts
1034
new_conflicts.update(conflict_pass(tt, conflicts))
1035
raise MalformedTransform(conflicts=conflicts)
1038
def conflict_pass(tt, conflicts):
1039
"""Resolve some classes of conflicts."""
1040
new_conflicts = set()
1041
for c_type, conflict in ((c[0], c) for c in conflicts):
1042
if c_type == 'duplicate id':
1043
tt.unversion_file(conflict[1])
1044
new_conflicts.add((c_type, 'Unversioned existing file',
1045
conflict[1], conflict[2], ))
1046
elif c_type == 'duplicate':
1047
# files that were renamed take precedence
1048
new_name = tt.final_name(conflict[1])+'.moved'
1049
final_parent = tt.final_parent(conflict[1])
1050
if tt.path_changed(conflict[1]):
1051
tt.adjust_path(new_name, final_parent, conflict[2])
1052
new_conflicts.add((c_type, 'Moved existing file to',
1053
conflict[2], conflict[1]))
1055
tt.adjust_path(new_name, final_parent, conflict[1])
1056
new_conflicts.add((c_type, 'Moved existing file to',
1057
conflict[1], conflict[2]))
1058
elif c_type == 'parent loop':
1059
# break the loop by undoing one of the ops that caused the loop
1061
while not tt.path_changed(cur):
1062
cur = tt.final_parent(cur)
1063
new_conflicts.add((c_type, 'Cancelled move', cur,
1064
tt.final_parent(cur),))
1065
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1067
elif c_type == 'missing parent':
1068
trans_id = conflict[1]
1070
tt.cancel_deletion(trans_id)
1071
new_conflicts.add((c_type, 'Not deleting', trans_id))
1073
tt.create_directory(trans_id)
1074
new_conflicts.add((c_type, 'Created directory.', trans_id))
1075
elif c_type == 'unversioned parent':
1076
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1077
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1078
return new_conflicts
1080
def cook_conflicts(raw_conflicts, tt):
1081
"""Generate a list of cooked conflicts, sorted by file path"""
1083
if conflict[2] is not None:
1084
return conflict[2], conflict[0]
1085
elif len(conflict) == 6:
1086
return conflict[4], conflict[0]
1088
return None, conflict[0]
1090
return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1092
def iter_cook_conflicts(raw_conflicts, tt):
1093
cooked_conflicts = []
1095
for conflict in raw_conflicts:
1096
c_type = conflict[0]
1097
action = conflict[1]
1098
modified_path = fp.get_path(conflict[2])
1099
modified_id = tt.final_file_id(conflict[2])
1100
if len(conflict) == 3:
1101
yield c_type, action, modified_path, modified_id
1103
conflicting_path = fp.get_path(conflict[3])
1104
conflicting_id = tt.final_file_id(conflict[3])
1105
yield (c_type, action, modified_path, modified_id,
1106
conflicting_path, conflicting_id)
1109
def conflicts_strings(conflicts):
1110
"""Generate strings for the provided conflicts"""
1111
for conflict in conflicts:
1112
conflict_type = conflict[0]
1113
if conflict_type == 'text conflict':
1114
yield 'Text conflict in %s' % conflict[2]
1115
elif conflict_type == 'contents conflict':
1116
yield 'Contents conflict in %s' % conflict[2]
1117
elif conflict_type == 'path conflict':
1118
yield 'Path conflict: %s / %s' % conflict[2:]
1119
elif conflict_type == 'duplicate id':
1120
vals = (conflict[4], conflict[1], conflict[2])
1121
yield 'Conflict adding id to %s. %s %s.' % vals
1122
elif conflict_type == 'duplicate':
1123
vals = (conflict[4], conflict[1], conflict[2])
1124
yield 'Conflict adding file %s. %s %s.' % vals
1125
elif conflict_type == 'parent loop':
1126
vals = (conflict[4], conflict[2], conflict[1])
1127
yield 'Conflict moving %s into %s. %s.' % vals
1128
elif conflict_type == 'unversioned parent':
1129
vals = (conflict[2], conflict[1])
1130
yield 'Conflict adding versioned files to %s. %s.' % vals
1131
elif conflict_type == 'missing parent':
1132
vals = (conflict[2], conflict[1])
1133
yield 'Conflict adding files to %s. %s.' % vals