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.progress import DummyProgress
28
from bzrlib.trace import mutter
31
ROOT_PARENT = "root-parent"
34
def unique_add(map, key, value):
36
raise DuplicateKey(key=key)
40
class TreeTransform(object):
41
"""Represent a tree transformation."""
42
def __init__(self, tree, pb=DummyProgress()):
43
"""Note: a write lock is taken on the tree.
45
Use TreeTransform.finalize() to release the lock
49
self._tree.lock_write()
51
control_files = self._tree._control_files
52
self._limbodir = control_files.controlfilename('limbo')
54
os.mkdir(self._limbodir)
56
if e.errno == errno.EEXIST:
57
raise ExistingLimbo(self._limbodir)
65
self._new_contents = {}
66
self._removed_contents = set()
67
self._new_executability = {}
69
self._non_present_ids = {}
71
self._removed_id = set()
72
self._tree_path_ids = {}
73
self._tree_id_paths = {}
74
self._new_root = self.get_id_tree(tree.get_root_id())
81
root = property(__get_root)
84
"""Release the working tree lock, if held, clean up limbo dir."""
85
if self._tree is None:
88
for trans_id, kind in self._new_contents.iteritems():
89
path = self._limbo_name(trans_id)
90
if kind == "directory":
95
os.rmdir(self._limbodir)
97
# We don't especially care *why* the dir is immortal.
98
raise ImmortalLimbo(self._limbodir)
103
def _assign_id(self):
104
"""Produce a new tranform id"""
105
new_id = "new-%s" % self._id_number
109
def create_path(self, name, parent):
110
"""Assign a transaction id to a new path"""
111
trans_id = self._assign_id()
112
unique_add(self._new_name, trans_id, name)
113
unique_add(self._new_parent, trans_id, parent)
116
def adjust_path(self, name, parent, trans_id):
117
"""Change the path that is assigned to a transaction id."""
118
if trans_id == self._new_root:
120
self._new_name[trans_id] = name
121
self._new_parent[trans_id] = parent
123
def adjust_root_path(self, name, parent):
124
"""Emulate moving the root by moving all children, instead.
126
We do this by undoing the association of root's transaction id with the
127
current tree. This allows us to create a new directory with that
128
transaction id. We unversion the root directory and version the
129
physically new directory, and hope someone versions the tree root
132
old_root = self._new_root
133
old_root_file_id = self.final_file_id(old_root)
134
# force moving all children of root
135
for child_id in self.iter_tree_children(old_root):
136
if child_id != parent:
137
self.adjust_path(self.final_name(child_id),
138
self.final_parent(child_id), child_id)
139
file_id = self.final_file_id(child_id)
140
if file_id is not None:
141
self.unversion_file(child_id)
142
self.version_file(file_id, child_id)
144
# the physical root needs a new transaction id
145
self._tree_path_ids.pop("")
146
self._tree_id_paths.pop(old_root)
147
self._new_root = self.get_id_tree(self._tree.get_root_id())
148
if parent == old_root:
149
parent = self._new_root
150
self.adjust_path(name, parent, old_root)
151
self.create_directory(old_root)
152
self.version_file(old_root_file_id, old_root)
153
self.unversion_file(self._new_root)
155
def get_id_tree(self, inventory_id):
156
"""Determine the transaction id of a working tree file.
158
This reflects only files that already exist, not ones that will be
159
added by transactions.
161
path = self._tree.inventory.id2path(inventory_id)
162
return self.get_tree_path_id(path)
164
def get_trans_id(self, file_id):
165
"""Determine or set the transaction id associated with a file ID.
166
A new id is only created for file_ids that were never present. If
167
a transaction has been unversioned, it is deliberately still returned.
168
(this will likely lead to an unversioned parent conflict.)
170
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
171
return self._r_new_id[file_id]
172
elif file_id in self._tree.inventory:
173
return self.get_id_tree(file_id)
174
elif file_id in self._non_present_ids:
175
return self._non_present_ids[file_id]
177
trans_id = self._assign_id()
178
self._non_present_ids[file_id] = trans_id
181
def canonical_path(self, path):
182
"""Get the canonical tree-relative path"""
183
# don't follow final symlinks
184
dirname, basename = os.path.split(self._tree.abspath(path))
185
dirname = os.path.realpath(dirname)
186
return self._tree.relpath(pathjoin(dirname, basename))
188
def get_tree_path_id(self, path):
189
"""Determine (and maybe set) the transaction ID for a tree path."""
190
path = self.canonical_path(path)
191
if path not in self._tree_path_ids:
192
self._tree_path_ids[path] = self._assign_id()
193
self._tree_id_paths[self._tree_path_ids[path]] = path
194
return self._tree_path_ids[path]
196
def get_tree_parent(self, trans_id):
197
"""Determine id of the parent in the tree."""
198
path = self._tree_id_paths[trans_id]
201
return self.get_tree_path_id(os.path.dirname(path))
203
def create_file(self, contents, trans_id, mode_id=None):
204
"""Schedule creation of a new file.
208
Contents is an iterator of strings, all of which will be written
209
to the target destination.
211
New file takes the permissions of any existing file with that id,
212
unless mode_id is specified.
214
f = file(self._limbo_name(trans_id), 'wb')
215
unique_add(self._new_contents, trans_id, 'file')
216
for segment in contents:
219
self._set_mode(trans_id, mode_id, S_ISREG)
221
def _set_mode(self, trans_id, mode_id, typefunc):
222
"""Set the mode of new file contents.
223
The mode_id is the existing file to get the mode from (often the same
224
as trans_id). The operation is only performed if there's a mode match
225
according to typefunc.
230
old_path = self._tree_id_paths[mode_id]
234
mode = os.stat(old_path).st_mode
236
if e.errno == errno.ENOENT:
241
os.chmod(self._limbo_name(trans_id), mode)
243
def create_directory(self, trans_id):
244
"""Schedule creation of a new directory.
246
See also new_directory.
248
os.mkdir(self._limbo_name(trans_id))
249
unique_add(self._new_contents, trans_id, 'directory')
251
def create_symlink(self, target, trans_id):
252
"""Schedule creation of a new symbolic link.
254
target is a bytestring.
255
See also new_symlink.
257
os.symlink(target, self._limbo_name(trans_id))
258
unique_add(self._new_contents, trans_id, 'symlink')
261
def delete_any(full_path):
262
"""Delete a file or directory."""
266
# We may be renaming a dangling inventory id
267
if e.errno != errno.EISDIR and e.errno != errno.EACCES:
271
def cancel_creation(self, trans_id):
272
"""Cancel the creation of new file contents."""
273
del self._new_contents[trans_id]
274
self.delete_any(self._limbo_name(trans_id))
276
def delete_contents(self, trans_id):
277
"""Schedule the contents of a path entry for deletion"""
278
self.tree_kind(trans_id)
279
self._removed_contents.add(trans_id)
281
def cancel_deletion(self, trans_id):
282
"""Cancel a scheduled deletion"""
283
self._removed_contents.remove(trans_id)
285
def unversion_file(self, trans_id):
286
"""Schedule a path entry to become unversioned"""
287
self._removed_id.add(trans_id)
289
def delete_versioned(self, trans_id):
290
"""Delete and unversion a versioned file"""
291
self.delete_contents(trans_id)
292
self.unversion_file(trans_id)
294
def set_executability(self, executability, trans_id):
295
"""Schedule setting of the 'execute' bit
296
To unschedule, set to None
298
if executability is None:
299
del self._new_executability[trans_id]
301
unique_add(self._new_executability, trans_id, executability)
303
def version_file(self, file_id, trans_id):
304
"""Schedule a file to become versioned."""
305
assert file_id is not None
306
unique_add(self._new_id, trans_id, file_id)
307
unique_add(self._r_new_id, file_id, trans_id)
309
def cancel_versioning(self, trans_id):
310
"""Undo a previous versioning of a file"""
311
file_id = self._new_id[trans_id]
312
del self._new_id[trans_id]
313
del self._r_new_id[file_id]
316
"""Determine the paths of all new and changed files"""
318
fp = FinalPaths(self)
319
for id_set in (self._new_name, self._new_parent, self._new_contents,
320
self._new_id, self._new_executability):
321
new_ids.update(id_set)
322
new_paths = [(fp.get_path(t), t) for t in new_ids]
326
def tree_kind(self, trans_id):
327
"""Determine the file kind in the working tree.
329
Raises NoSuchFile if the file does not exist
331
path = self._tree_id_paths.get(trans_id)
333
raise NoSuchFile(None)
335
return file_kind(self._tree.abspath(path))
337
if e.errno != errno.ENOENT:
340
raise NoSuchFile(path)
342
def final_kind(self, trans_id):
343
"""Determine the final file kind, after any changes applied.
345
Raises NoSuchFile if the file does not exist/has no contents.
346
(It is conceivable that a path would be created without the
347
corresponding contents insertion command)
349
if trans_id in self._new_contents:
350
return self._new_contents[trans_id]
351
elif trans_id in self._removed_contents:
352
raise NoSuchFile(None)
354
return self.tree_kind(trans_id)
356
def get_tree_file_id(self, trans_id):
357
"""Determine the file id associated with the trans_id in the tree"""
359
path = self._tree_id_paths[trans_id]
361
# the file is a new, unversioned file, or invalid trans_id
363
# the file is old; the old id is still valid
364
if self._new_root == trans_id:
365
return self._tree.inventory.root.file_id
366
return self._tree.inventory.path2id(path)
368
def final_file_id(self, trans_id):
369
"""Determine the file id after any changes are applied, or None.
371
None indicates that the file will not be versioned after changes are
375
# there is a new id for this file
376
assert self._new_id[trans_id] is not None
377
return self._new_id[trans_id]
379
if trans_id in self._removed_id:
381
return self.get_tree_file_id(trans_id)
383
def inactive_file_id(self, trans_id):
384
"""Return the inactive file_id associated with a transaction id.
385
That is, the one in the tree or in non_present_ids.
386
The file_id may actually be active, too.
388
file_id = self.get_tree_file_id(trans_id)
389
if file_id is not None:
391
for key, value in self._non_present_ids.iteritems():
392
if value == trans_id:
395
def final_parent(self, trans_id):
396
"""Determine the parent file_id, after any changes are applied.
398
ROOT_PARENT is returned for the tree root.
401
return self._new_parent[trans_id]
403
return self.get_tree_parent(trans_id)
405
def final_name(self, trans_id):
406
"""Determine the final filename, after all changes are applied."""
408
return self._new_name[trans_id]
410
return os.path.basename(self._tree_id_paths[trans_id])
412
def _by_parent(self):
413
"""Return a map of parent: children for known parents.
415
Only new paths and parents of tree files with assigned ids are used.
418
items = list(self._new_parent.iteritems())
419
items.extend((t, self.final_parent(t)) for t in
420
self._tree_id_paths.keys())
421
for trans_id, parent_id in items:
422
if parent_id not in by_parent:
423
by_parent[parent_id] = set()
424
by_parent[parent_id].add(trans_id)
427
def path_changed(self, trans_id):
428
"""Return True if a trans_id's path has changed."""
429
return trans_id in self._new_name or trans_id in self._new_parent
431
def find_conflicts(self):
432
"""Find any violations of inventory or filesystem invariants"""
433
if self.__done is True:
434
raise ReusingTransform()
436
# ensure all children of all existent parents are known
437
# all children of non-existent parents are known, by definition.
438
self._add_tree_children()
439
by_parent = self._by_parent()
440
conflicts.extend(self._unversioned_parents(by_parent))
441
conflicts.extend(self._parent_loops())
442
conflicts.extend(self._duplicate_entries(by_parent))
443
conflicts.extend(self._duplicate_ids())
444
conflicts.extend(self._parent_type_conflicts(by_parent))
445
conflicts.extend(self._improper_versioning())
446
conflicts.extend(self._executability_conflicts())
447
conflicts.extend(self._overwrite_conflicts())
450
def _add_tree_children(self):
451
"""Add all the children of all active parents to the known paths.
453
Active parents are those which gain children, and those which are
454
removed. This is a necessary first step in detecting conflicts.
456
parents = self._by_parent().keys()
457
parents.extend([t for t in self._removed_contents if
458
self.tree_kind(t) == 'directory'])
459
for trans_id in self._removed_id:
460
file_id = self.get_tree_file_id(trans_id)
461
if self._tree.inventory[file_id].kind in ('directory',
463
parents.append(trans_id)
465
for parent_id in parents:
466
# ensure that all children are registered with the transaction
467
list(self.iter_tree_children(parent_id))
469
def iter_tree_children(self, parent_id):
470
"""Iterate through the entry's tree children, if any"""
472
path = self._tree_id_paths[parent_id]
476
children = os.listdir(self._tree.abspath(path))
478
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
482
for child in children:
483
childpath = joinpath(path, child)
484
if childpath == BZRDIR:
486
yield self.get_tree_path_id(childpath)
488
def _parent_loops(self):
489
"""No entry should be its own ancestor"""
491
for trans_id in self._new_parent:
494
while parent_id is not ROOT_PARENT:
496
parent_id = self.final_parent(parent_id)
497
if parent_id == trans_id:
498
conflicts.append(('parent loop', trans_id))
499
if parent_id in seen:
503
def _unversioned_parents(self, by_parent):
504
"""If parent directories are versioned, children must be versioned."""
506
for parent_id, children in by_parent.iteritems():
507
if parent_id is ROOT_PARENT:
509
if self.final_file_id(parent_id) is not None:
511
for child_id in children:
512
if self.final_file_id(child_id) is not None:
513
conflicts.append(('unversioned parent', parent_id))
517
def _improper_versioning(self):
518
"""Cannot version a file with no contents, or a bad type.
520
However, existing entries with no contents are okay.
523
for trans_id in self._new_id.iterkeys():
525
kind = self.final_kind(trans_id)
527
conflicts.append(('versioning no contents', trans_id))
529
if not InventoryEntry.versionable_kind(kind):
530
conflicts.append(('versioning bad kind', trans_id, kind))
533
def _executability_conflicts(self):
534
"""Check for bad executability changes.
536
Only versioned files may have their executability set, because
537
1. only versioned entries can have executability under windows
538
2. only files can be executable. (The execute bit on a directory
539
does not indicate searchability)
542
for trans_id in self._new_executability:
543
if self.final_file_id(trans_id) is None:
544
conflicts.append(('unversioned executability', trans_id))
547
non_file = self.final_kind(trans_id) != "file"
551
conflicts.append(('non-file executability', trans_id))
554
def _overwrite_conflicts(self):
555
"""Check for overwrites (not permitted on Win32)"""
557
for trans_id in self._new_contents:
559
self.tree_kind(trans_id)
562
if trans_id not in self._removed_contents:
563
conflicts.append(('overwrite', trans_id,
564
self.final_name(trans_id)))
567
def _duplicate_entries(self, by_parent):
568
"""No directory may have two entries with the same name."""
570
for children in by_parent.itervalues():
571
name_ids = [(self.final_name(t), t) for t in children]
575
for name, trans_id in name_ids:
576
if name == last_name:
577
conflicts.append(('duplicate', last_trans_id, trans_id,
580
last_trans_id = trans_id
583
def _duplicate_ids(self):
584
"""Each inventory id may only be used once"""
586
removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
588
active_tree_ids = set((f for f in self._tree.inventory if
589
f not in removed_tree_ids))
590
for trans_id, file_id in self._new_id.iteritems():
591
if file_id in active_tree_ids:
592
old_trans_id = self.get_id_tree(file_id)
593
conflicts.append(('duplicate id', old_trans_id, trans_id))
596
def _parent_type_conflicts(self, by_parent):
597
"""parents must have directory 'contents'."""
599
for parent_id, children in by_parent.iteritems():
600
if parent_id is ROOT_PARENT:
602
if not self._any_contents(children):
604
for child in children:
606
self.final_kind(child)
610
kind = self.final_kind(parent_id)
614
conflicts.append(('missing parent', parent_id))
615
elif kind != "directory":
616
conflicts.append(('non-directory parent', parent_id))
619
def _any_contents(self, trans_ids):
620
"""Return true if any of the trans_ids, will have contents."""
621
for trans_id in trans_ids:
623
kind = self.final_kind(trans_id)
630
"""Apply all changes to the inventory and filesystem.
632
If filesystem or inventory conflicts are present, MalformedTransform
635
conflicts = self.find_conflicts()
636
if len(conflicts) != 0:
637
raise MalformedTransform(conflicts=conflicts)
639
inv = self._tree.inventory
640
self._apply_removals(inv, limbo_inv)
641
self._apply_insertions(inv, limbo_inv)
642
self._tree._write_inventory(inv)
646
def _limbo_name(self, trans_id):
647
"""Generate the limbo name of a file"""
648
return pathjoin(self._limbodir, trans_id)
650
def _apply_removals(self, inv, limbo_inv):
651
"""Perform tree operations that remove directory/inventory names.
653
That is, delete files that are to be deleted, and put any files that
654
need renaming into limbo. This must be done in strict child-to-parent
657
tree_paths = list(self._tree_path_ids.iteritems())
658
tree_paths.sort(reverse=True)
659
for num, data in enumerate(tree_paths):
660
path, trans_id = data
661
self._pb.update('removing file', num+1, len(tree_paths))
662
full_path = self._tree.abspath(path)
663
if trans_id in self._removed_contents:
664
self.delete_any(full_path)
665
elif trans_id in self._new_name or trans_id in self._new_parent:
667
os.rename(full_path, self._limbo_name(trans_id))
669
if e.errno != errno.ENOENT:
671
if trans_id in self._removed_id:
672
if trans_id == self._new_root:
673
file_id = self._tree.inventory.root.file_id
675
file_id = self.get_tree_file_id(trans_id)
677
elif trans_id in self._new_name or trans_id in self._new_parent:
678
file_id = self.get_tree_file_id(trans_id)
679
if file_id is not None:
680
limbo_inv[trans_id] = inv[file_id]
684
def _apply_insertions(self, inv, limbo_inv):
685
"""Perform tree operations that insert directory/inventory names.
687
That is, create any files that need to be created, and restore from
688
limbo any files that needed renaming. This must be done in strict
689
parent-to-child order.
691
new_paths = self.new_paths()
692
for num, (path, trans_id) in enumerate(new_paths):
693
self._pb.update('adding file', num+1, len(new_paths))
695
kind = self._new_contents[trans_id]
697
kind = contents = None
698
if trans_id in self._new_contents or self.path_changed(trans_id):
699
full_path = self._tree.abspath(path)
701
os.rename(self._limbo_name(trans_id), full_path)
703
# We may be renaming a dangling inventory id
704
if e.errno != errno.ENOENT:
706
if trans_id in self._new_contents:
707
del self._new_contents[trans_id]
709
if trans_id in self._new_id:
711
kind = file_kind(self._tree.abspath(path))
712
inv.add_path(path, kind, self._new_id[trans_id])
713
elif trans_id in self._new_name or trans_id in self._new_parent:
714
entry = limbo_inv.get(trans_id)
715
if entry is not None:
716
entry.name = self.final_name(trans_id)
717
parent_path = os.path.dirname(path)
718
entry.parent_id = self._tree.inventory.path2id(parent_path)
721
# requires files and inventory entries to be in place
722
if trans_id in self._new_executability:
723
self._set_executability(path, inv, trans_id)
726
def _set_executability(self, path, inv, trans_id):
727
"""Set the executability of versioned files """
728
file_id = inv.path2id(path)
729
new_executability = self._new_executability[trans_id]
730
inv[file_id].executable = new_executability
731
if supports_executable():
732
abspath = self._tree.abspath(path)
733
current_mode = os.stat(abspath).st_mode
734
if new_executability:
737
to_mode = current_mode | (0100 & ~umask)
738
# Enable x-bit for others only if they can read it.
739
if current_mode & 0004:
740
to_mode |= 0001 & ~umask
741
if current_mode & 0040:
742
to_mode |= 0010 & ~umask
744
to_mode = current_mode & ~0111
745
os.chmod(abspath, to_mode)
747
def _new_entry(self, name, parent_id, file_id):
748
"""Helper function to create a new filesystem entry."""
749
trans_id = self.create_path(name, parent_id)
750
if file_id is not None:
751
self.version_file(file_id, trans_id)
754
def new_file(self, name, parent_id, contents, file_id=None,
756
"""Convenience method to create files.
758
name is the name of the file to create.
759
parent_id is the transaction id of the parent directory of the file.
760
contents is an iterator of bytestrings, which will be used to produce
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_file(contents, trans_id)
766
if executable is not None:
767
self.set_executability(executable, trans_id)
770
def new_directory(self, name, parent_id, file_id=None):
771
"""Convenience method to create directories.
773
name is the name of the directory to create.
774
parent_id is the transaction id of the parent directory of the
776
file_id is the inventory ID of the directory, if it is to be versioned.
778
trans_id = self._new_entry(name, parent_id, file_id)
779
self.create_directory(trans_id)
782
def new_symlink(self, name, parent_id, target, file_id=None):
783
"""Convenience method to create symbolic link.
785
name is the name of the symlink to create.
786
parent_id is the transaction id of the parent directory of the symlink.
787
target is a bytestring of the target of the symlink.
788
file_id is the inventory ID of the file, if it is to be versioned.
790
trans_id = self._new_entry(name, parent_id, file_id)
791
self.create_symlink(target, trans_id)
794
def joinpath(parent, child):
795
"""Join tree-relative paths, handling the tree root specially"""
796
if parent is None or parent == "":
799
return pathjoin(parent, child)
802
class FinalPaths(object):
803
"""Make path calculation cheap by memoizing paths.
805
The underlying tree must not be manipulated between calls, or else
806
the results will likely be incorrect.
808
def __init__(self, transform):
809
object.__init__(self)
810
self._known_paths = {}
811
self.transform = transform
813
def _determine_path(self, trans_id):
814
if trans_id == self.transform.root:
816
name = self.transform.final_name(trans_id)
817
parent_id = self.transform.final_parent(trans_id)
818
if parent_id == self.transform.root:
821
return pathjoin(self.get_path(parent_id), name)
823
def get_path(self, trans_id):
824
"""Find the final path associated with a trans_id"""
825
if trans_id not in self._known_paths:
826
self._known_paths[trans_id] = self._determine_path(trans_id)
827
return self._known_paths[trans_id]
829
def topology_sorted_ids(tree):
830
"""Determine the topological order of the ids in a tree"""
831
file_ids = list(tree)
832
file_ids.sort(key=tree.id2path)
835
def build_tree(tree, wt):
836
"""Create working tree for a branch, using a Transaction."""
838
tt = TreeTransform(wt)
840
file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
841
file_ids = topology_sorted_ids(tree)
842
for file_id in file_ids:
843
entry = tree.inventory[file_id]
844
if entry.parent_id is None:
846
if entry.parent_id not in file_trans_id:
847
raise repr(entry.parent_id)
848
parent_id = file_trans_id[entry.parent_id]
849
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
854
def new_by_entry(tt, entry, parent_id, tree):
855
"""Create a new file according to its inventory entry"""
859
contents = tree.get_file(entry.file_id).readlines()
860
executable = tree.is_executable(entry.file_id)
861
return tt.new_file(name, parent_id, contents, entry.file_id,
863
elif kind == 'directory':
864
return tt.new_directory(name, parent_id, entry.file_id)
865
elif kind == 'symlink':
866
target = entry.get_symlink_target(file_id)
867
return tt.new_symlink(name, parent_id, target, file_id)
869
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
870
"""Create new file contents according to an inventory entry."""
871
if entry.kind == "file":
873
lines = tree.get_file(entry.file_id).readlines()
874
tt.create_file(lines, trans_id, mode_id=mode_id)
875
elif entry.kind == "symlink":
876
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
877
elif entry.kind == "directory":
878
tt.create_directory(trans_id)
880
def create_entry_executability(tt, entry, trans_id):
881
"""Set the executability of a trans_id according to an inventory entry"""
882
if entry.kind == "file":
883
tt.set_executability(entry.executable, trans_id)
886
def find_interesting(working_tree, target_tree, filenames):
887
"""Find the ids corresponding to specified filenames."""
889
interesting_ids = None
891
interesting_ids = set()
892
for tree_path in filenames:
893
for tree in (working_tree, target_tree):
895
file_id = tree.inventory.path2id(tree_path)
896
if file_id is not None:
897
interesting_ids.add(file_id)
900
raise NotVersionedError(path=tree_path)
901
return interesting_ids
904
def change_entry(tt, file_id, working_tree, target_tree,
905
get_trans_id, backups, trans_id):
906
"""Replace a file_id's contents with those from a target tree."""
907
e_trans_id = get_trans_id(file_id)
908
entry = target_tree.inventory[file_id]
909
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
915
tt.delete_contents(e_trans_id)
917
parent_trans_id = get_trans_id(entry.parent_id)
918
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
919
tt.unversion_file(e_trans_id)
920
e_trans_id = tt.create_path(entry.name, parent_trans_id)
921
tt.version_file(file_id, e_trans_id)
922
trans_id[file_id] = e_trans_id
923
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
924
create_entry_executability(tt, entry, e_trans_id)
927
tt.set_executability(entry.executable, e_trans_id)
928
if tt.final_name(e_trans_id) != entry.name:
931
parent_id = tt.final_parent(e_trans_id)
932
parent_file_id = tt.final_file_id(parent_id)
933
if parent_file_id != entry.parent_id:
938
parent_trans_id = get_trans_id(entry.parent_id)
939
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
942
def _entry_changes(file_id, entry, working_tree):
943
"""Determine in which ways the inventory entry has changed.
945
Returns booleans: has_contents, content_mod, meta_mod
946
has_contents means there are currently contents, but they differ
947
contents_mod means contents need to be modified
948
meta_mod means the metadata needs to be modified
950
cur_entry = working_tree.inventory[file_id]
952
working_kind = working_tree.kind(file_id)
955
if e.errno != errno.ENOENT:
960
if has_contents is True:
961
real_e_kind = entry.kind
962
if real_e_kind == 'root_directory':
963
real_e_kind = 'directory'
964
if real_e_kind != working_kind:
965
contents_mod, meta_mod = True, False
967
cur_entry._read_tree_state(working_tree.id2path(file_id),
969
contents_mod, meta_mod = entry.detect_changes(cur_entry)
970
return has_contents, contents_mod, meta_mod
973
def revert(working_tree, target_tree, filenames, backups=False):
974
"""Revert a working tree's contents to those of a target tree."""
975
interesting_ids = find_interesting(working_tree, target_tree, filenames)
976
def interesting(file_id):
977
return interesting_ids is None or file_id in interesting_ids
979
tt = TreeTransform(working_tree)
982
def get_trans_id(file_id):
984
return trans_id[file_id]
986
return tt.get_id_tree(file_id)
988
for file_id in topology_sorted_ids(target_tree):
989
if not interesting(file_id):
991
if file_id not in working_tree.inventory:
992
entry = target_tree.inventory[file_id]
993
parent_id = get_trans_id(entry.parent_id)
994
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
995
trans_id[file_id] = e_trans_id
997
change_entry(tt, file_id, working_tree, target_tree,
998
get_trans_id, backups, trans_id)
999
for file_id in working_tree:
1000
if not interesting(file_id):
1002
if file_id not in target_tree:
1003
tt.unversion_file(tt.get_id_tree(file_id))
1004
conflicts = resolve_conflicts(tt)
1010
def resolve_conflicts(tt, pb=DummyProgress()):
1011
"""Make many conflict-resolution attempts, but die if they fail"""
1012
new_conflicts = set()
1015
pb.update('Resolution pass', n+1, 10)
1016
conflicts = tt.find_conflicts()
1017
if len(conflicts) == 0:
1018
return new_conflicts
1019
new_conflicts.update(conflict_pass(tt, conflicts))
1020
raise MalformedTransform(conflicts=conflicts)
1025
def conflict_pass(tt, conflicts):
1026
"""Resolve some classes of conflicts."""
1027
new_conflicts = set()
1028
for c_type, conflict in ((c[0], c) for c in conflicts):
1029
if c_type == 'duplicate id':
1030
tt.unversion_file(conflict[1])
1031
new_conflicts.add((c_type, 'Unversioned existing file',
1032
conflict[1], conflict[2], ))
1033
elif c_type == 'duplicate':
1034
# files that were renamed take precedence
1035
new_name = tt.final_name(conflict[1])+'.moved'
1036
final_parent = tt.final_parent(conflict[1])
1037
if tt.path_changed(conflict[1]):
1038
tt.adjust_path(new_name, final_parent, conflict[2])
1039
new_conflicts.add((c_type, 'Moved existing file to',
1040
conflict[2], conflict[1]))
1042
tt.adjust_path(new_name, final_parent, conflict[1])
1043
new_conflicts.add((c_type, 'Moved existing file to',
1044
conflict[1], conflict[2]))
1045
elif c_type == 'parent loop':
1046
# break the loop by undoing one of the ops that caused the loop
1048
while not tt.path_changed(cur):
1049
cur = tt.final_parent(cur)
1050
new_conflicts.add((c_type, 'Cancelled move', cur,
1051
tt.final_parent(cur),))
1052
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1054
elif c_type == 'missing parent':
1055
trans_id = conflict[1]
1057
tt.cancel_deletion(trans_id)
1058
new_conflicts.add((c_type, 'Not deleting', trans_id))
1060
tt.create_directory(trans_id)
1061
new_conflicts.add((c_type, 'Created directory.', trans_id))
1062
elif c_type == 'unversioned parent':
1063
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1064
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1065
return new_conflicts
1067
def cook_conflicts(raw_conflicts, tt):
1068
"""Generate a list of cooked conflicts, sorted by file path"""
1070
if conflict[2] is not None:
1071
return conflict[2], conflict[0]
1072
elif len(conflict) == 6:
1073
return conflict[4], conflict[0]
1075
return None, conflict[0]
1077
return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1079
def iter_cook_conflicts(raw_conflicts, tt):
1080
cooked_conflicts = []
1082
for conflict in raw_conflicts:
1083
c_type = conflict[0]
1084
action = conflict[1]
1085
modified_path = fp.get_path(conflict[2])
1086
modified_id = tt.final_file_id(conflict[2])
1087
if len(conflict) == 3:
1088
yield c_type, action, modified_path, modified_id
1090
conflicting_path = fp.get_path(conflict[3])
1091
conflicting_id = tt.final_file_id(conflict[3])
1092
yield (c_type, action, modified_path, modified_id,
1093
conflicting_path, conflicting_id)
1096
def conflicts_strings(conflicts):
1097
"""Generate strings for the provided conflicts"""
1098
for conflict in conflicts:
1099
conflict_type = conflict[0]
1100
if conflict_type == 'text conflict':
1101
yield 'Text conflict in %s' % conflict[2]
1102
elif conflict_type == 'contents conflict':
1103
yield 'Contents conflict in %s' % conflict[2]
1104
elif conflict_type == 'path conflict':
1105
yield 'Path conflict: %s / %s' % conflict[2:]
1106
elif conflict_type == 'duplicate id':
1107
vals = (conflict[4], conflict[1], conflict[2])
1108
yield 'Conflict adding id to %s. %s %s.' % vals
1109
elif conflict_type == 'duplicate':
1110
vals = (conflict[4], conflict[1], conflict[2])
1111
yield 'Conflict adding file %s. %s %s.' % vals
1112
elif conflict_type == 'parent loop':
1113
vals = (conflict[4], conflict[2], conflict[1])
1114
yield 'Conflict moving %s into %s. %s.' % vals
1115
elif conflict_type == 'unversioned parent':
1116
vals = (conflict[2], conflict[1])
1117
yield 'Conflict adding versioned files to %s. %s.' % vals
1118
elif conflict_type == 'missing parent':
1119
vals = (conflict[2], conflict[1])
1120
yield 'Conflict adding files to %s. %s.' % vals