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
ExistingLimbo, ImmortalLimbo)
25
from bzrlib.inventory import InventoryEntry
26
from bzrlib.osutils import file_kind, supports_executable, pathjoin
27
from bzrlib.trace import mutter
29
ROOT_PARENT = "root-parent"
31
def unique_add(map, key, value):
33
raise DuplicateKey(key=key)
36
class TreeTransform(object):
37
"""Represent a tree transformation."""
38
def __init__(self, tree):
39
"""Note: a write lock is taken on the tree.
41
Use TreeTransform.finalize() to release the lock
45
self._tree.lock_write()
47
control_files = self._tree._control_files
48
self._limbodir = control_files.controlfilename('limbo')
50
os.mkdir(self._limbodir)
52
if e.errno == errno.EEXIST:
53
raise ExistingLimbo(self._limbodir)
61
self._new_contents = {}
62
self._removed_contents = set()
63
self._new_executability = {}
65
self._non_present_ids = {}
67
self._removed_id = set()
68
self._tree_path_ids = {}
69
self._tree_id_paths = {}
70
self._new_root = self.get_id_tree(tree.get_root_id())
76
root = property(__get_root)
79
"""Release the working tree lock, if held, clean up limbo dir."""
80
if self._tree is None:
83
for trans_id, kind in self._new_contents.iteritems():
84
path = self._limbo_name(trans_id)
85
if kind == "directory":
90
os.rmdir(self._limbodir)
92
# We don't especially care *why* the dir is immortal.
93
raise ImmortalLimbo(self._limbodir)
99
"""Produce a new tranform id"""
100
new_id = "new-%s" % self._id_number
104
def create_path(self, name, parent):
105
"""Assign a transaction id to a new path"""
106
trans_id = self._assign_id()
107
unique_add(self._new_name, trans_id, name)
108
unique_add(self._new_parent, trans_id, parent)
111
def adjust_path(self, name, parent, trans_id):
112
"""Change the path that is assigned to a transaction id."""
113
if trans_id == self._new_root:
115
self._new_name[trans_id] = name
116
self._new_parent[trans_id] = parent
118
def adjust_root_path(self, name, parent):
119
"""Emulate moving the root by moving all children, instead.
121
We do this by undoing the association of root's transaction id with the
122
current tree. This allows us to create a new directory with that
123
transaction id. We unversion the root directory and version the
124
physically new directory, and hope someone versions the tree root
127
old_root = self._new_root
128
old_root_file_id = self.final_file_id(old_root)
129
# force moving all children of root
130
for child_id in self.iter_tree_children(old_root):
131
if child_id != parent:
132
self.adjust_path(self.final_name(child_id),
133
self.final_parent(child_id), child_id)
134
file_id = self.final_file_id(child_id)
135
if file_id is not None:
136
self.unversion_file(child_id)
137
self.version_file(file_id, child_id)
139
# the physical root needs a new transaction id
140
self._tree_path_ids.pop("")
141
self._tree_id_paths.pop(old_root)
142
self._new_root = self.get_id_tree(self._tree.get_root_id())
143
if parent == old_root:
144
parent = self._new_root
145
self.adjust_path(name, parent, old_root)
146
self.create_directory(old_root)
147
self.version_file(old_root_file_id, old_root)
148
self.unversion_file(self._new_root)
150
def get_id_tree(self, inventory_id):
151
"""Determine the transaction id of a working tree file.
153
This reflects only files that already exist, not ones that will be
154
added by transactions.
156
path = self._tree.inventory.id2path(inventory_id)
157
return self.get_tree_path_id(path)
159
def get_trans_id(self, file_id):
160
"""Determine or set the transaction id associated with a file ID.
161
A new id is only created for file_ids that were never present. If
162
a transaction has been unversioned, it is deliberately still returned.
163
(this will likely lead to an unversioned parent conflict.)
165
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
166
return self._r_new_id[file_id]
167
elif file_id in self._tree.inventory:
168
return self.get_id_tree(file_id)
169
elif file_id in self._non_present_ids:
170
return self._non_present_ids[file_id]
172
trans_id = self._assign_id()
173
self._non_present_ids[file_id] = trans_id
176
def canonical_path(self, path):
177
"""Get the canonical tree-relative path"""
178
# don't follow final symlinks
179
dirname, basename = os.path.split(self._tree.abspath(path))
180
dirname = os.path.realpath(dirname)
181
return self._tree.relpath(os.path.join(dirname, basename))
183
def get_tree_path_id(self, path):
184
"""Determine (and maybe set) the transaction ID for a tree path."""
185
path = self.canonical_path(path)
186
if path not in self._tree_path_ids:
187
self._tree_path_ids[path] = self._assign_id()
188
self._tree_id_paths[self._tree_path_ids[path]] = path
189
return self._tree_path_ids[path]
191
def get_tree_parent(self, trans_id):
192
"""Determine id of the parent in the tree."""
193
path = self._tree_id_paths[trans_id]
196
return self.get_tree_path_id(os.path.dirname(path))
198
def create_file(self, contents, trans_id, mode_id=None):
199
"""Schedule creation of a new file.
203
Contents is an iterator of strings, all of which will be written
204
to the target destination.
206
New file takes the permissions of any existing file with that id,
207
unless mode_id is specified.
209
f = file(self._limbo_name(trans_id), 'wb')
210
unique_add(self._new_contents, trans_id, 'file')
211
for segment in contents:
214
self._set_mode(trans_id, mode_id, S_ISREG)
216
def _set_mode(self, trans_id, mode_id, typefunc):
217
"""Set the mode of new file contents.
218
The mode_id is the existing file to get the mode from (often the same
219
as trans_id). The operation is only performed if there's a mode match
220
according to typefunc.
225
old_path = self._tree_id_paths[mode_id]
229
mode = os.stat(old_path).st_mode
231
if e.errno == errno.ENOENT:
236
os.chmod(self._limbo_name(trans_id), mode)
238
def create_directory(self, trans_id):
239
"""Schedule creation of a new directory.
241
See also new_directory.
243
os.mkdir(self._limbo_name(trans_id))
244
unique_add(self._new_contents, trans_id, 'directory')
246
def create_symlink(self, target, trans_id):
247
"""Schedule creation of a new symbolic link.
249
target is a bytestring.
250
See also new_symlink.
252
os.symlink(target, self._limbo_name(trans_id))
253
unique_add(self._new_contents, trans_id, 'symlink')
256
def delete_any(full_path):
257
"""Delete a file or directory."""
261
# We may be renaming a dangling inventory id
262
if e.errno != errno.EISDIR and e.errno != errno.EACCES:
266
def cancel_creation(self, trans_id):
267
"""Cancel the creation of new file contents."""
268
del self._new_contents[trans_id]
269
self.delete_any(self._limbo_name(trans_id))
271
def delete_contents(self, trans_id):
272
"""Schedule the contents of a path entry for deletion"""
273
self.tree_kind(trans_id)
274
self._removed_contents.add(trans_id)
276
def cancel_deletion(self, trans_id):
277
"""Cancel a scheduled deletion"""
278
self._removed_contents.remove(trans_id)
280
def unversion_file(self, trans_id):
281
"""Schedule a path entry to become unversioned"""
282
self._removed_id.add(trans_id)
284
def delete_versioned(self, trans_id):
285
"""Delete and unversion a versioned file"""
286
self.delete_contents(trans_id)
287
self.unversion_file(trans_id)
289
def set_executability(self, executability, trans_id):
290
"""Schedule setting of the 'execute' bit"""
291
if executability is None:
292
del self._new_executability[trans_id]
294
unique_add(self._new_executability, trans_id, executability)
296
def version_file(self, file_id, trans_id):
297
"""Schedule a file to become versioned."""
298
assert file_id is not None
299
unique_add(self._new_id, trans_id, file_id)
300
unique_add(self._r_new_id, file_id, trans_id)
302
def cancel_versioning(self, trans_id):
303
"""Undo a previous versioning of a file"""
304
file_id = self._new_id[trans_id]
305
del self._new_id[trans_id]
306
del self._r_new_id[file_id]
309
"""Determine the paths of all new and changed files"""
311
fp = FinalPaths(self)
312
for id_set in (self._new_name, self._new_parent, self._new_contents,
313
self._new_id, self._new_executability):
314
new_ids.update(id_set)
315
new_paths = [(fp.get_path(t), t) for t in new_ids]
319
def tree_kind(self, trans_id):
320
"""Determine the file kind in the working tree.
322
Raises NoSuchFile if the file does not exist
324
path = self._tree_id_paths.get(trans_id)
326
raise NoSuchFile(None)
328
return file_kind(self._tree.abspath(path))
330
if e.errno != errno.ENOENT:
333
raise NoSuchFile(path)
335
def final_kind(self, trans_id):
336
"""Determine the final file kind, after any changes applied.
338
Raises NoSuchFile if the file does not exist/has no contents.
339
(It is conceivable that a path would be created without the
340
corresponding contents insertion command)
342
if trans_id in self._new_contents:
343
return self._new_contents[trans_id]
344
elif trans_id in self._removed_contents:
345
raise NoSuchFile(None)
347
return self.tree_kind(trans_id)
349
def get_tree_file_id(self, trans_id):
350
"""Determine the file id associated with the trans_id in the tree"""
352
path = self._tree_id_paths[trans_id]
354
# the file is a new, unversioned file, or invalid trans_id
356
# the file is old; the old id is still valid
357
if self._new_root == trans_id:
358
return self._tree.inventory.root.file_id
359
return self._tree.inventory.path2id(path)
361
def final_file_id(self, trans_id):
362
"""Determine the file id after any changes are applied, or None.
364
None indicates that the file will not be versioned after changes are
368
# there is a new id for this file
369
assert self._new_id[trans_id] is not None
370
return self._new_id[trans_id]
372
if trans_id in self._removed_id:
374
return self.get_tree_file_id(trans_id)
377
def inactive_file_id(self, trans_id):
378
"""Return the inactive file_id associated with a transaction id.
379
That is, the one in the tree or in non_present_ids.
380
The file_id may actually be active, too.
382
file_id = self.get_tree_file_id(trans_id)
383
if file_id is not None:
385
for key, value in self._non_present_ids.iteritems():
386
if value == trans_id:
389
def final_parent(self, trans_id):
390
"""Determine the parent file_id, after any changes are applied.
392
ROOT_PARENT is returned for the tree root.
395
return self._new_parent[trans_id]
397
return self.get_tree_parent(trans_id)
399
def final_name(self, trans_id):
400
"""Determine the final filename, after all changes are applied."""
402
return self._new_name[trans_id]
404
return os.path.basename(self._tree_id_paths[trans_id])
406
def _by_parent(self):
407
"""Return a map of parent: children for known parents.
409
Only new paths and parents of tree files with assigned ids are used.
412
items = list(self._new_parent.iteritems())
413
items.extend((t, self.final_parent(t)) for t in
414
self._tree_id_paths.keys())
415
for trans_id, parent_id in items:
416
if parent_id not in by_parent:
417
by_parent[parent_id] = set()
418
by_parent[parent_id].add(trans_id)
421
def path_changed(self, trans_id):
422
"""Return True if a trans_id's path has changed."""
423
return trans_id in self._new_name or trans_id in self._new_parent
425
def find_conflicts(self):
426
"""Find any violations of inventory or filesystem invariants"""
427
if self.__done is True:
428
raise ReusingTransform()
430
# ensure all children of all existent parents are known
431
# all children of non-existent parents are known, by definition.
432
self._add_tree_children()
433
by_parent = self._by_parent()
434
conflicts.extend(self._unversioned_parents(by_parent))
435
conflicts.extend(self._parent_loops())
436
conflicts.extend(self._duplicate_entries(by_parent))
437
conflicts.extend(self._duplicate_ids())
438
conflicts.extend(self._parent_type_conflicts(by_parent))
439
conflicts.extend(self._improper_versioning())
440
conflicts.extend(self._executability_conflicts())
441
conflicts.extend(self._overwrite_conflicts())
444
def _add_tree_children(self):
445
"""Add all the children of all active parents to the known paths.
447
Active parents are those which gain children, and those which are
448
removed. This is a necessary first step in detecting conflicts.
450
parents = self._by_parent().keys()
451
parents.extend([t for t in self._removed_contents if
452
self.tree_kind(t) == 'directory'])
453
for trans_id in self._removed_id:
454
file_id = self.get_tree_file_id(trans_id)
455
if self._tree.inventory[file_id].kind in ('directory',
457
parents.append(trans_id)
459
for parent_id in parents:
460
# ensure that all children are registered with the transaction
461
list(self.iter_tree_children(parent_id))
463
def iter_tree_children(self, parent_id):
464
"""Iterate through the entry's tree children, if any"""
466
path = self._tree_id_paths[parent_id]
470
children = os.listdir(self._tree.abspath(path))
472
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
476
for child in children:
477
childpath = joinpath(path, child)
478
if childpath == BZRDIR:
480
yield self.get_tree_path_id(childpath)
482
def _parent_loops(self):
483
"""No entry should be its own ancestor"""
485
for trans_id in self._new_parent:
488
while parent_id is not ROOT_PARENT:
490
parent_id = self.final_parent(parent_id)
491
if parent_id == trans_id:
492
conflicts.append(('parent loop', trans_id))
493
if parent_id in seen:
497
def _unversioned_parents(self, by_parent):
498
"""If parent directories are versioned, children must be versioned."""
500
for parent_id, children in by_parent.iteritems():
501
if parent_id is ROOT_PARENT:
503
if self.final_file_id(parent_id) is not None:
505
for child_id in children:
506
if self.final_file_id(child_id) is not None:
507
conflicts.append(('unversioned parent', parent_id))
511
def _improper_versioning(self):
512
"""Cannot version a file with no contents, or a bad type.
514
However, existing entries with no contents are okay.
517
for trans_id in self._new_id.iterkeys():
519
kind = self.final_kind(trans_id)
521
conflicts.append(('versioning no contents', trans_id))
523
if not InventoryEntry.versionable_kind(kind):
524
conflicts.append(('versioning bad kind', trans_id, kind))
527
def _executability_conflicts(self):
528
"""Check for bad executability changes.
530
Only versioned files may have their executability set, because
531
1. only versioned entries can have executability under windows
532
2. only files can be executable. (The execute bit on a directory
533
does not indicate searchability)
536
for trans_id in self._new_executability:
537
if self.final_file_id(trans_id) is None:
538
conflicts.append(('unversioned executability', trans_id))
541
non_file = self.final_kind(trans_id) != "file"
545
conflicts.append(('non-file executability', trans_id))
548
def _overwrite_conflicts(self):
549
"""Check for overwrites (not permitted on Win32)"""
551
for trans_id in self._new_contents:
553
self.tree_kind(trans_id)
556
if trans_id not in self._removed_contents:
557
conflicts.append(('overwrite', trans_id,
558
self.final_name(trans_id)))
561
def _duplicate_entries(self, by_parent):
562
"""No directory may have two entries with the same name."""
564
for children in by_parent.itervalues():
565
name_ids = [(self.final_name(t), t) for t in children]
569
for name, trans_id in name_ids:
570
if name == last_name:
571
conflicts.append(('duplicate', last_trans_id, trans_id,
574
last_trans_id = trans_id
577
def _duplicate_ids(self):
578
"""Each inventory id may only be used once"""
580
removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
582
active_tree_ids = set((f for f in self._tree.inventory if
583
f not in removed_tree_ids))
584
for trans_id, file_id in self._new_id.iteritems():
585
if file_id in active_tree_ids:
586
old_trans_id = self.get_id_tree(file_id)
587
conflicts.append(('duplicate id', old_trans_id, trans_id))
590
def _parent_type_conflicts(self, by_parent):
591
"""parents must have directory 'contents'."""
593
for parent_id, children in by_parent.iteritems():
594
if parent_id is ROOT_PARENT:
596
if not self._any_contents(children):
598
for child in children:
600
self.final_kind(child)
604
kind = self.final_kind(parent_id)
608
conflicts.append(('missing parent', parent_id))
609
elif kind != "directory":
610
conflicts.append(('non-directory parent', parent_id))
613
def _any_contents(self, trans_ids):
614
"""Return true if any of the trans_ids, will have contents."""
615
for trans_id in trans_ids:
617
kind = self.final_kind(trans_id)
624
"""Apply all changes to the inventory and filesystem.
626
If filesystem or inventory conflicts are present, MalformedTransform
629
conflicts = self.find_conflicts()
630
if len(conflicts) != 0:
631
raise MalformedTransform(conflicts=conflicts)
633
inv = self._tree.inventory
634
self._apply_removals(inv, limbo_inv)
635
self._apply_insertions(inv, limbo_inv)
636
self._tree._write_inventory(inv)
640
def _limbo_name(self, trans_id):
641
"""Generate the limbo name of a file"""
642
return os.path.join(self._limbodir, trans_id)
644
def _apply_removals(self, inv, limbo_inv):
645
"""Perform tree operations that remove directory/inventory names.
647
That is, delete files that are to be deleted, and put any files that
648
need renaming into limbo. This must be done in strict child-to-parent
651
tree_paths = list(self._tree_path_ids.iteritems())
652
tree_paths.sort(reverse=True)
653
for path, trans_id in tree_paths:
654
full_path = self._tree.abspath(path)
655
if trans_id in self._removed_contents:
656
self.delete_any(full_path)
657
elif trans_id in self._new_name or trans_id in self._new_parent:
659
os.rename(full_path, self._limbo_name(trans_id))
661
if e.errno != errno.ENOENT:
663
if trans_id in self._removed_id:
664
if trans_id == self._new_root:
665
file_id = self._tree.inventory.root.file_id
667
file_id = self.get_tree_file_id(trans_id)
669
elif trans_id in self._new_name or trans_id in self._new_parent:
670
file_id = self.get_tree_file_id(trans_id)
671
if file_id is not None:
672
limbo_inv[trans_id] = inv[file_id]
675
def _apply_insertions(self, inv, limbo_inv):
676
"""Perform tree operations that insert directory/inventory names.
678
That is, create any files that need to be created, and restore from
679
limbo any files that needed renaming. This must be done in strict
680
parent-to-child order.
682
for path, trans_id in self.new_paths():
684
kind = self._new_contents[trans_id]
686
kind = contents = None
687
if trans_id in self._new_contents or self.path_changed(trans_id):
688
full_path = self._tree.abspath(path)
690
os.rename(self._limbo_name(trans_id), full_path)
692
# We may be renaming a dangling inventory id
693
if e.errno != errno.ENOENT:
695
if trans_id in self._new_contents:
696
del self._new_contents[trans_id]
698
if trans_id in self._new_id:
700
kind = file_kind(self._tree.abspath(path))
701
inv.add_path(path, kind, self._new_id[trans_id])
702
elif trans_id in self._new_name or trans_id in self._new_parent:
703
entry = limbo_inv.get(trans_id)
704
if entry is not None:
705
entry.name = self.final_name(trans_id)
706
parent_path = os.path.dirname(path)
707
entry.parent_id = self._tree.inventory.path2id(parent_path)
710
# requires files and inventory entries to be in place
711
if trans_id in self._new_executability:
712
self._set_executability(path, inv, trans_id)
714
def _set_executability(self, path, inv, trans_id):
715
"""Set the executability of versioned files """
716
file_id = inv.path2id(path)
717
new_executability = self._new_executability[trans_id]
718
inv[file_id].executable = new_executability
719
if supports_executable():
720
abspath = self._tree.abspath(path)
721
current_mode = os.stat(abspath).st_mode
722
if new_executability:
725
to_mode = current_mode | (0100 & ~umask)
726
# Enable x-bit for others only if they can read it.
727
if current_mode & 0004:
728
to_mode |= 0001 & ~umask
729
if current_mode & 0040:
730
to_mode |= 0010 & ~umask
732
to_mode = current_mode & ~0111
733
os.chmod(abspath, to_mode)
735
def _new_entry(self, name, parent_id, file_id):
736
"""Helper function to create a new filesystem entry."""
737
trans_id = self.create_path(name, parent_id)
738
if file_id is not None:
739
self.version_file(file_id, trans_id)
742
def new_file(self, name, parent_id, contents, file_id=None,
744
"""Convenience method to create files.
746
name is the name of the file to create.
747
parent_id is the transaction id of the parent directory of the file.
748
contents is an iterator of bytestrings, which will be used to produce
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_file(contents, trans_id)
754
if executable is not None:
755
self.set_executability(executable, trans_id)
758
def new_directory(self, name, parent_id, file_id=None):
759
"""Convenience method to create directories.
761
name is the name of the directory to create.
762
parent_id is the transaction id of the parent directory of the
764
file_id is the inventory ID of the directory, if it is to be versioned.
766
trans_id = self._new_entry(name, parent_id, file_id)
767
self.create_directory(trans_id)
770
def new_symlink(self, name, parent_id, target, file_id=None):
771
"""Convenience method to create symbolic link.
773
name is the name of the symlink to create.
774
parent_id is the transaction id of the parent directory of the symlink.
775
target is a bytestring of the target of the symlink.
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_symlink(target, trans_id)
782
def joinpath(parent, child):
783
"""Join tree-relative paths, handling the tree root specially"""
784
if parent is None or parent == "":
787
return os.path.join(parent, child)
789
class FinalPaths(object):
790
"""Make path calculation cheap by memoizing paths.
792
The underlying tree must not be manipulated between calls, or else
793
the results will likely be incorrect.
795
def __init__(self, transform):
796
object.__init__(self)
797
self._known_paths = {}
798
self.transform = transform
800
def _determine_path(self, trans_id):
801
if trans_id == self.transform.root:
803
name = self.transform.final_name(trans_id)
804
parent_id = self.transform.final_parent(trans_id)
805
if parent_id == self.transform.root:
808
return os.path.join(self.get_path(parent_id), name)
810
def get_path(self, trans_id):
811
"""Find the final path associated with a trans_id"""
812
if trans_id not in self._known_paths:
813
self._known_paths[trans_id] = self._determine_path(trans_id)
814
return self._known_paths[trans_id]
816
def topology_sorted_ids(tree):
817
"""Determine the topological order of the ids in a tree"""
818
file_ids = list(tree)
819
file_ids.sort(key=tree.id2path)
822
def build_tree(tree, wt):
823
"""Create working tree for a branch, using a Transaction."""
825
tt = TreeTransform(wt)
827
file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
828
file_ids = topology_sorted_ids(tree)
829
for file_id in file_ids:
830
entry = tree.inventory[file_id]
831
if entry.parent_id is None:
833
if entry.parent_id not in file_trans_id:
834
raise repr(entry.parent_id)
835
parent_id = file_trans_id[entry.parent_id]
836
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
841
def new_by_entry(tt, entry, parent_id, tree):
842
"""Create a new file according to its inventory entry"""
846
contents = tree.get_file(entry.file_id).readlines()
847
executable = tree.is_executable(entry.file_id)
848
return tt.new_file(name, parent_id, contents, entry.file_id,
850
elif kind == 'directory':
851
return tt.new_directory(name, parent_id, entry.file_id)
852
elif kind == 'symlink':
853
target = entry.get_symlink_target(file_id)
854
return tt.new_symlink(name, parent_id, target, file_id)
856
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
857
"""Create new file contents according to an inventory entry."""
858
if entry.kind == "file":
860
lines = tree.get_file(entry.file_id).readlines()
861
tt.create_file(lines, trans_id, mode_id=mode_id)
862
elif entry.kind == "symlink":
863
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
864
elif entry.kind == "directory":
865
tt.create_directory(trans_id)
867
def create_entry_executability(tt, entry, trans_id):
868
"""Set the executability of a trans_id according to an inventory entry"""
869
if entry.kind == "file":
870
tt.set_executability(entry.executable, trans_id)
873
def find_interesting(working_tree, target_tree, filenames):
874
"""Find the ids corresponding to specified filenames."""
876
interesting_ids = None
878
interesting_ids = set()
879
for tree_path in filenames:
880
for tree in (working_tree, target_tree):
882
file_id = tree.inventory.path2id(tree_path)
883
if file_id is not None:
884
interesting_ids.add(file_id)
887
raise NotVersionedError(path=tree_path)
888
return interesting_ids
891
def change_entry(tt, file_id, working_tree, target_tree,
892
get_trans_id, backups, trans_id):
893
"""Replace a file_id's contents with those from a target tree."""
894
e_trans_id = get_trans_id(file_id)
895
entry = target_tree.inventory[file_id]
896
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
902
tt.delete_contents(e_trans_id)
904
parent_trans_id = get_trans_id(entry.parent_id)
905
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
906
tt.unversion_file(e_trans_id)
907
e_trans_id = tt.create_path(entry.name, parent_trans_id)
908
tt.version_file(file_id, e_trans_id)
909
trans_id[file_id] = e_trans_id
910
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
911
create_entry_executability(tt, entry, e_trans_id)
914
tt.set_executability(entry.executable, e_trans_id)
915
if tt.final_name(e_trans_id) != entry.name:
918
parent_id = tt.final_parent(e_trans_id)
919
parent_file_id = tt.final_file_id(parent_id)
920
if parent_file_id != entry.parent_id:
925
parent_trans_id = get_trans_id(entry.parent_id)
926
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
929
def _entry_changes(file_id, entry, working_tree):
930
"""Determine in which ways the inventory entry has changed.
932
Returns booleans: has_contents, content_mod, meta_mod
933
has_contents means there are currently contents, but they differ
934
contents_mod means contents need to be modified
935
meta_mod means the metadata needs to be modified
937
cur_entry = working_tree.inventory[file_id]
939
working_kind = working_tree.kind(file_id)
942
if e.errno != errno.ENOENT:
947
if has_contents is True:
948
real_e_kind = entry.kind
949
if real_e_kind == 'root_directory':
950
real_e_kind = 'directory'
951
if real_e_kind != working_kind:
952
contents_mod, meta_mod = True, False
954
cur_entry._read_tree_state(working_tree.id2path(file_id),
956
contents_mod, meta_mod = entry.detect_changes(cur_entry)
957
return has_contents, contents_mod, meta_mod
960
def revert(working_tree, target_tree, filenames, backups=False):
961
"""Revert a working tree's contents to those of a target tree."""
962
interesting_ids = find_interesting(working_tree, target_tree, filenames)
963
def interesting(file_id):
964
return interesting_ids is None or file_id in interesting_ids
966
tt = TreeTransform(working_tree)
969
def get_trans_id(file_id):
971
return trans_id[file_id]
973
return tt.get_id_tree(file_id)
975
for file_id in topology_sorted_ids(target_tree):
976
if not interesting(file_id):
978
if file_id not in working_tree.inventory:
979
entry = target_tree.inventory[file_id]
980
parent_id = get_trans_id(entry.parent_id)
981
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
982
trans_id[file_id] = e_trans_id
984
change_entry(tt, file_id, working_tree, target_tree,
985
get_trans_id, backups, trans_id)
986
for file_id in working_tree:
987
if not interesting(file_id):
989
if file_id not in target_tree:
990
tt.unversion_file(tt.get_id_tree(file_id))
991
resolve_conflicts(tt)
997
def resolve_conflicts(tt):
998
"""Make many conflict-resolution attempts, but die if they fail"""
1000
conflicts = tt.find_conflicts()
1001
if len(conflicts) == 0:
1003
conflict_pass(tt, conflicts)
1004
raise MalformedTransform(conflicts=conflicts)
1007
def conflict_pass(tt, conflicts):
1008
"""Resolve some classes of conflicts."""
1009
for c_type, conflict in ((c[0], c) for c in conflicts):
1010
if c_type == 'duplicate id':
1011
tt.unversion_file(conflict[1])
1012
elif c_type == 'duplicate':
1013
# files that were renamed take precedence
1014
new_name = tt.final_name(conflict[1])+'.moved'
1015
final_parent = tt.final_parent(conflict[1])
1016
if tt.path_changed(conflict[1]):
1017
tt.adjust_path(new_name, final_parent, conflict[2])
1019
tt.adjust_path(new_name, final_parent, conflict[1])
1020
elif c_type == 'parent loop':
1021
# break the loop by undoing one of the ops that caused the loop
1023
while not tt.path_changed(cur):
1024
cur = tt.final_parent(cur)
1025
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1026
elif c_type == 'missing parent':
1027
trans_id = conflict[1]
1029
tt.cancel_deletion(trans_id)
1031
tt.create_directory(trans_id)
1032
elif c_type == 'unversioned parent':
1033
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])