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.progress import DummyProgress
27
from bzrlib.trace import mutter, warning
30
ROOT_PARENT = "root-parent"
33
def unique_add(map, key, value):
35
raise DuplicateKey(key=key)
39
class TreeTransform(object):
40
"""Represent a tree transformation.
42
This object is designed to support incremental generation of the transform,
45
It is easy to produce malformed transforms, but they are generally
46
harmless. Attempting to apply a malformed transform will cause an
47
exception to be raised before any modifications are made to the tree.
49
Many kinds of malformed transforms can be corrected with the
50
resolve_conflicts function. The remaining ones indicate programming error,
51
such as trying to create a file with no path.
53
Two sets of file creation methods are supplied. Convenience methods are:
58
These are composed of the low-level methods:
60
* create_file or create_directory or create_symlink
64
def __init__(self, tree, pb=DummyProgress()):
65
"""Note: a write lock is taken on the tree.
67
Use TreeTransform.finalize() to release the lock
71
self._tree.lock_write()
73
control_files = self._tree._control_files
74
self._limbodir = control_files.controlfilename('limbo')
76
os.mkdir(self._limbodir)
78
if e.errno == errno.EEXIST:
79
raise ExistingLimbo(self._limbodir)
87
self._new_contents = {}
88
self._removed_contents = set()
89
self._new_executability = {}
91
self._non_present_ids = {}
93
self._removed_id = set()
94
self._tree_path_ids = {}
95
self._tree_id_paths = {}
96
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
100
def __get_root(self):
101
return self._new_root
103
root = property(__get_root)
106
"""Release the working tree lock, if held, clean up limbo dir."""
107
if self._tree is None:
110
for trans_id, kind in self._new_contents.iteritems():
111
path = self._limbo_name(trans_id)
112
if kind == "directory":
117
os.rmdir(self._limbodir)
119
# We don't especially care *why* the dir is immortal.
120
raise ImmortalLimbo(self._limbodir)
125
def _assign_id(self):
126
"""Produce a new tranform id"""
127
new_id = "new-%s" % self._id_number
131
def create_path(self, name, parent):
132
"""Assign a transaction id to a new path"""
133
trans_id = self._assign_id()
134
unique_add(self._new_name, trans_id, name)
135
unique_add(self._new_parent, trans_id, parent)
138
def adjust_path(self, name, parent, trans_id):
139
"""Change the path that is assigned to a transaction id."""
140
if trans_id == self._new_root:
142
self._new_name[trans_id] = name
143
self._new_parent[trans_id] = parent
145
def adjust_root_path(self, name, parent):
146
"""Emulate moving the root by moving all children, instead.
148
We do this by undoing the association of root's transaction id with the
149
current tree. This allows us to create a new directory with that
150
transaction id. We unversion the root directory and version the
151
physically new directory, and hope someone versions the tree root
154
old_root = self._new_root
155
old_root_file_id = self.final_file_id(old_root)
156
# force moving all children of root
157
for child_id in self.iter_tree_children(old_root):
158
if child_id != parent:
159
self.adjust_path(self.final_name(child_id),
160
self.final_parent(child_id), child_id)
161
file_id = self.final_file_id(child_id)
162
if file_id is not None:
163
self.unversion_file(child_id)
164
self.version_file(file_id, child_id)
166
# the physical root needs a new transaction id
167
self._tree_path_ids.pop("")
168
self._tree_id_paths.pop(old_root)
169
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
170
if parent == old_root:
171
parent = self._new_root
172
self.adjust_path(name, parent, old_root)
173
self.create_directory(old_root)
174
self.version_file(old_root_file_id, old_root)
175
self.unversion_file(self._new_root)
177
def trans_id_tree_file_id(self, inventory_id):
178
"""Determine the transaction id of a working tree file.
180
This reflects only files that already exist, not ones that will be
181
added by transactions.
183
path = self._tree.inventory.id2path(inventory_id)
184
return self.trans_id_tree_path(path)
186
def trans_id_file_id(self, file_id):
187
"""Determine or set the transaction id associated with a file ID.
188
A new id is only created for file_ids that were never present. If
189
a transaction has been unversioned, it is deliberately still returned.
190
(this will likely lead to an unversioned parent conflict.)
192
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
193
return self._r_new_id[file_id]
194
elif file_id in self._tree.inventory:
195
return self.trans_id_tree_file_id(file_id)
196
elif file_id in self._non_present_ids:
197
return self._non_present_ids[file_id]
199
trans_id = self._assign_id()
200
self._non_present_ids[file_id] = trans_id
203
def canonical_path(self, path):
204
"""Get the canonical tree-relative path"""
205
# don't follow final symlinks
206
dirname, basename = os.path.split(self._tree.abspath(path))
207
dirname = os.path.realpath(dirname)
208
return self._tree.relpath(pathjoin(dirname, basename))
210
def trans_id_tree_path(self, path):
211
"""Determine (and maybe set) the transaction ID for a tree path."""
212
path = self.canonical_path(path)
213
if path not in self._tree_path_ids:
214
self._tree_path_ids[path] = self._assign_id()
215
self._tree_id_paths[self._tree_path_ids[path]] = path
216
return self._tree_path_ids[path]
218
def get_tree_parent(self, trans_id):
219
"""Determine id of the parent in the tree."""
220
path = self._tree_id_paths[trans_id]
223
return self.trans_id_tree_path(os.path.dirname(path))
225
def create_file(self, contents, trans_id, mode_id=None):
226
"""Schedule creation of a new file.
230
Contents is an iterator of strings, all of which will be written
231
to the target destination.
233
New file takes the permissions of any existing file with that id,
234
unless mode_id is specified.
236
f = file(self._limbo_name(trans_id), 'wb')
237
unique_add(self._new_contents, trans_id, 'file')
238
for segment in contents:
241
self._set_mode(trans_id, mode_id, S_ISREG)
243
def _set_mode(self, trans_id, mode_id, typefunc):
244
"""Set the mode of new file contents.
245
The mode_id is the existing file to get the mode from (often the same
246
as trans_id). The operation is only performed if there's a mode match
247
according to typefunc.
252
old_path = self._tree_id_paths[mode_id]
256
mode = os.stat(old_path).st_mode
258
if e.errno == errno.ENOENT:
263
os.chmod(self._limbo_name(trans_id), mode)
265
def create_directory(self, trans_id):
266
"""Schedule creation of a new directory.
268
See also new_directory.
270
os.mkdir(self._limbo_name(trans_id))
271
unique_add(self._new_contents, trans_id, 'directory')
273
def create_symlink(self, target, trans_id):
274
"""Schedule creation of a new symbolic link.
276
target is a bytestring.
277
See also new_symlink.
279
os.symlink(target, self._limbo_name(trans_id))
280
unique_add(self._new_contents, trans_id, 'symlink')
283
def delete_any(full_path):
284
"""Delete a file or directory."""
288
# We may be renaming a dangling inventory id
289
if e.errno not in (errno.EISDIR, errno.EACCES, errno.EPERM):
293
def cancel_creation(self, trans_id):
294
"""Cancel the creation of new file contents."""
295
del self._new_contents[trans_id]
296
self.delete_any(self._limbo_name(trans_id))
298
def delete_contents(self, trans_id):
299
"""Schedule the contents of a path entry for deletion"""
300
self.tree_kind(trans_id)
301
self._removed_contents.add(trans_id)
303
def cancel_deletion(self, trans_id):
304
"""Cancel a scheduled deletion"""
305
self._removed_contents.remove(trans_id)
307
def unversion_file(self, trans_id):
308
"""Schedule a path entry to become unversioned"""
309
self._removed_id.add(trans_id)
311
def delete_versioned(self, trans_id):
312
"""Delete and unversion a versioned file"""
313
self.delete_contents(trans_id)
314
self.unversion_file(trans_id)
316
def set_executability(self, executability, trans_id):
317
"""Schedule setting of the 'execute' bit
318
To unschedule, set to None
320
if executability is None:
321
del self._new_executability[trans_id]
323
unique_add(self._new_executability, trans_id, executability)
325
def version_file(self, file_id, trans_id):
326
"""Schedule a file to become versioned."""
327
assert file_id is not None
328
unique_add(self._new_id, trans_id, file_id)
329
unique_add(self._r_new_id, file_id, trans_id)
331
def cancel_versioning(self, trans_id):
332
"""Undo a previous versioning of a file"""
333
file_id = self._new_id[trans_id]
334
del self._new_id[trans_id]
335
del self._r_new_id[file_id]
338
"""Determine the paths of all new and changed files"""
340
fp = FinalPaths(self)
341
for id_set in (self._new_name, self._new_parent, self._new_contents,
342
self._new_id, self._new_executability):
343
new_ids.update(id_set)
344
new_paths = [(fp.get_path(t), t) for t in new_ids]
348
def tree_kind(self, trans_id):
349
"""Determine the file kind in the working tree.
351
Raises NoSuchFile if the file does not exist
353
path = self._tree_id_paths.get(trans_id)
355
raise NoSuchFile(None)
357
return file_kind(self._tree.abspath(path))
359
if e.errno != errno.ENOENT:
362
raise NoSuchFile(path)
364
def final_kind(self, trans_id):
365
"""Determine the final file kind, after any changes applied.
367
Raises NoSuchFile if the file does not exist/has no contents.
368
(It is conceivable that a path would be created without the
369
corresponding contents insertion command)
371
if trans_id in self._new_contents:
372
return self._new_contents[trans_id]
373
elif trans_id in self._removed_contents:
374
raise NoSuchFile(None)
376
return self.tree_kind(trans_id)
378
def tree_file_id(self, trans_id):
379
"""Determine the file id associated with the trans_id in the tree"""
381
path = self._tree_id_paths[trans_id]
383
# the file is a new, unversioned file, or invalid trans_id
385
# the file is old; the old id is still valid
386
if self._new_root == trans_id:
387
return self._tree.inventory.root.file_id
388
return self._tree.inventory.path2id(path)
390
def final_file_id(self, trans_id):
391
"""Determine the file id after any changes are applied, or None.
393
None indicates that the file will not be versioned after changes are
397
# there is a new id for this file
398
assert self._new_id[trans_id] is not None
399
return self._new_id[trans_id]
401
if trans_id in self._removed_id:
403
return self.tree_file_id(trans_id)
405
def inactive_file_id(self, trans_id):
406
"""Return the inactive file_id associated with a transaction id.
407
That is, the one in the tree or in non_present_ids.
408
The file_id may actually be active, too.
410
file_id = self.tree_file_id(trans_id)
411
if file_id is not None:
413
for key, value in self._non_present_ids.iteritems():
414
if value == trans_id:
417
def final_parent(self, trans_id):
418
"""Determine the parent file_id, after any changes are applied.
420
ROOT_PARENT is returned for the tree root.
423
return self._new_parent[trans_id]
425
return self.get_tree_parent(trans_id)
427
def final_name(self, trans_id):
428
"""Determine the final filename, after all changes are applied."""
430
return self._new_name[trans_id]
432
return os.path.basename(self._tree_id_paths[trans_id])
434
def _by_parent(self):
435
"""Return a map of parent: children for known parents.
437
Only new paths and parents of tree files with assigned ids are used.
440
items = list(self._new_parent.iteritems())
441
items.extend((t, self.final_parent(t)) for t in
442
self._tree_id_paths.keys())
443
for trans_id, parent_id in items:
444
if parent_id not in by_parent:
445
by_parent[parent_id] = set()
446
by_parent[parent_id].add(trans_id)
449
def path_changed(self, trans_id):
450
"""Return True if a trans_id's path has changed."""
451
return trans_id in self._new_name or trans_id in self._new_parent
453
def find_conflicts(self):
454
"""Find any violations of inventory or filesystem invariants"""
455
if self.__done is True:
456
raise ReusingTransform()
458
# ensure all children of all existent parents are known
459
# all children of non-existent parents are known, by definition.
460
self._add_tree_children()
461
by_parent = self._by_parent()
462
conflicts.extend(self._unversioned_parents(by_parent))
463
conflicts.extend(self._parent_loops())
464
conflicts.extend(self._duplicate_entries(by_parent))
465
conflicts.extend(self._duplicate_ids())
466
conflicts.extend(self._parent_type_conflicts(by_parent))
467
conflicts.extend(self._improper_versioning())
468
conflicts.extend(self._executability_conflicts())
469
conflicts.extend(self._overwrite_conflicts())
472
def _add_tree_children(self):
473
"""Add all the children of all active parents to the known paths.
475
Active parents are those which gain children, and those which are
476
removed. This is a necessary first step in detecting conflicts.
478
parents = self._by_parent().keys()
479
parents.extend([t for t in self._removed_contents if
480
self.tree_kind(t) == 'directory'])
481
for trans_id in self._removed_id:
482
file_id = self.tree_file_id(trans_id)
483
if self._tree.inventory[file_id].kind in ('directory',
485
parents.append(trans_id)
487
for parent_id in parents:
488
# ensure that all children are registered with the transaction
489
list(self.iter_tree_children(parent_id))
491
def iter_tree_children(self, parent_id):
492
"""Iterate through the entry's tree children, if any"""
494
path = self._tree_id_paths[parent_id]
498
children = os.listdir(self._tree.abspath(path))
500
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
504
for child in children:
505
childpath = joinpath(path, child)
506
if self._tree.is_control_filename(childpath):
508
yield self.trans_id_tree_path(childpath)
510
def _parent_loops(self):
511
"""No entry should be its own ancestor"""
513
for trans_id in self._new_parent:
516
while parent_id is not ROOT_PARENT:
518
parent_id = self.final_parent(parent_id)
519
if parent_id == trans_id:
520
conflicts.append(('parent loop', trans_id))
521
if parent_id in seen:
525
def _unversioned_parents(self, by_parent):
526
"""If parent directories are versioned, children must be versioned."""
528
for parent_id, children in by_parent.iteritems():
529
if parent_id is ROOT_PARENT:
531
if self.final_file_id(parent_id) is not None:
533
for child_id in children:
534
if self.final_file_id(child_id) is not None:
535
conflicts.append(('unversioned parent', parent_id))
539
def _improper_versioning(self):
540
"""Cannot version a file with no contents, or a bad type.
542
However, existing entries with no contents are okay.
545
for trans_id in self._new_id.iterkeys():
547
kind = self.final_kind(trans_id)
549
conflicts.append(('versioning no contents', trans_id))
551
if not InventoryEntry.versionable_kind(kind):
552
conflicts.append(('versioning bad kind', trans_id, kind))
555
def _executability_conflicts(self):
556
"""Check for bad executability changes.
558
Only versioned files may have their executability set, because
559
1. only versioned entries can have executability under windows
560
2. only files can be executable. (The execute bit on a directory
561
does not indicate searchability)
564
for trans_id in self._new_executability:
565
if self.final_file_id(trans_id) is None:
566
conflicts.append(('unversioned executability', trans_id))
569
non_file = self.final_kind(trans_id) != "file"
573
conflicts.append(('non-file executability', trans_id))
576
def _overwrite_conflicts(self):
577
"""Check for overwrites (not permitted on Win32)"""
579
for trans_id in self._new_contents:
581
self.tree_kind(trans_id)
584
if trans_id not in self._removed_contents:
585
conflicts.append(('overwrite', trans_id,
586
self.final_name(trans_id)))
589
def _duplicate_entries(self, by_parent):
590
"""No directory may have two entries with the same name."""
592
for children in by_parent.itervalues():
593
name_ids = [(self.final_name(t), t) for t in children]
597
for name, trans_id in name_ids:
598
if name == last_name:
599
conflicts.append(('duplicate', last_trans_id, trans_id,
602
last_trans_id = trans_id
605
def _duplicate_ids(self):
606
"""Each inventory id may only be used once"""
608
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
610
active_tree_ids = set((f for f in self._tree.inventory if
611
f not in removed_tree_ids))
612
for trans_id, file_id in self._new_id.iteritems():
613
if file_id in active_tree_ids:
614
old_trans_id = self.trans_id_tree_file_id(file_id)
615
conflicts.append(('duplicate id', old_trans_id, trans_id))
618
def _parent_type_conflicts(self, by_parent):
619
"""parents must have directory 'contents'."""
621
for parent_id, children in by_parent.iteritems():
622
if parent_id is ROOT_PARENT:
624
if not self._any_contents(children):
626
for child in children:
628
self.final_kind(child)
632
kind = self.final_kind(parent_id)
636
conflicts.append(('missing parent', parent_id))
637
elif kind != "directory":
638
conflicts.append(('non-directory parent', parent_id))
641
def _any_contents(self, trans_ids):
642
"""Return true if any of the trans_ids, will have contents."""
643
for trans_id in trans_ids:
645
kind = self.final_kind(trans_id)
652
"""Apply all changes to the inventory and filesystem.
654
If filesystem or inventory conflicts are present, MalformedTransform
657
conflicts = self.find_conflicts()
658
if len(conflicts) != 0:
659
raise MalformedTransform(conflicts=conflicts)
661
inv = self._tree.inventory
662
self._apply_removals(inv, limbo_inv)
663
self._apply_insertions(inv, limbo_inv)
664
self._tree._write_inventory(inv)
668
def _limbo_name(self, trans_id):
669
"""Generate the limbo name of a file"""
670
return pathjoin(self._limbodir, trans_id)
672
def _apply_removals(self, inv, limbo_inv):
673
"""Perform tree operations that remove directory/inventory names.
675
That is, delete files that are to be deleted, and put any files that
676
need renaming into limbo. This must be done in strict child-to-parent
679
tree_paths = list(self._tree_path_ids.iteritems())
680
tree_paths.sort(reverse=True)
681
for num, data in enumerate(tree_paths):
682
path, trans_id = data
683
self._pb.update('removing file', num+1, len(tree_paths))
684
full_path = self._tree.abspath(path)
685
if trans_id in self._removed_contents:
686
self.delete_any(full_path)
687
elif trans_id in self._new_name or trans_id in self._new_parent:
689
os.rename(full_path, self._limbo_name(trans_id))
691
if e.errno != errno.ENOENT:
693
if trans_id in self._removed_id:
694
if trans_id == self._new_root:
695
file_id = self._tree.inventory.root.file_id
697
file_id = self.tree_file_id(trans_id)
699
elif trans_id in self._new_name or trans_id in self._new_parent:
700
file_id = self.tree_file_id(trans_id)
701
if file_id is not None:
702
limbo_inv[trans_id] = inv[file_id]
706
def _apply_insertions(self, inv, limbo_inv):
707
"""Perform tree operations that insert directory/inventory names.
709
That is, create any files that need to be created, and restore from
710
limbo any files that needed renaming. This must be done in strict
711
parent-to-child order.
713
new_paths = self.new_paths()
714
for num, (path, trans_id) in enumerate(new_paths):
715
self._pb.update('adding file', num+1, len(new_paths))
717
kind = self._new_contents[trans_id]
719
kind = contents = None
720
if trans_id in self._new_contents or self.path_changed(trans_id):
721
full_path = self._tree.abspath(path)
723
os.rename(self._limbo_name(trans_id), full_path)
725
# We may be renaming a dangling inventory id
726
if e.errno != errno.ENOENT:
728
if trans_id in self._new_contents:
729
del self._new_contents[trans_id]
731
if trans_id in self._new_id:
733
kind = file_kind(self._tree.abspath(path))
734
inv.add_path(path, kind, self._new_id[trans_id])
735
elif trans_id in self._new_name or trans_id in self._new_parent:
736
entry = limbo_inv.get(trans_id)
737
if entry is not None:
738
entry.name = self.final_name(trans_id)
739
parent_path = os.path.dirname(path)
740
entry.parent_id = self._tree.inventory.path2id(parent_path)
743
# requires files and inventory entries to be in place
744
if trans_id in self._new_executability:
745
self._set_executability(path, inv, trans_id)
748
def _set_executability(self, path, inv, trans_id):
749
"""Set the executability of versioned files """
750
file_id = inv.path2id(path)
751
new_executability = self._new_executability[trans_id]
752
inv[file_id].executable = new_executability
753
if supports_executable():
754
abspath = self._tree.abspath(path)
755
current_mode = os.stat(abspath).st_mode
756
if new_executability:
759
to_mode = current_mode | (0100 & ~umask)
760
# Enable x-bit for others only if they can read it.
761
if current_mode & 0004:
762
to_mode |= 0001 & ~umask
763
if current_mode & 0040:
764
to_mode |= 0010 & ~umask
766
to_mode = current_mode & ~0111
767
os.chmod(abspath, to_mode)
769
def _new_entry(self, name, parent_id, file_id):
770
"""Helper function to create a new filesystem entry."""
771
trans_id = self.create_path(name, parent_id)
772
if file_id is not None:
773
self.version_file(file_id, trans_id)
776
def new_file(self, name, parent_id, contents, file_id=None,
778
"""Convenience method to create files.
780
name is the name of the file to create.
781
parent_id is the transaction id of the parent directory of the file.
782
contents is an iterator of bytestrings, which will be used to produce
784
file_id is the inventory ID of the file, if it is to be versioned.
786
trans_id = self._new_entry(name, parent_id, file_id)
787
self.create_file(contents, trans_id)
788
if executable is not None:
789
self.set_executability(executable, trans_id)
792
def new_directory(self, name, parent_id, file_id=None):
793
"""Convenience method to create directories.
795
name is the name of the directory to create.
796
parent_id is the transaction id of the parent directory of the
798
file_id is the inventory ID of the directory, if it is to be versioned.
800
trans_id = self._new_entry(name, parent_id, file_id)
801
self.create_directory(trans_id)
804
def new_symlink(self, name, parent_id, target, file_id=None):
805
"""Convenience method to create symbolic link.
807
name is the name of the symlink to create.
808
parent_id is the transaction id of the parent directory of the symlink.
809
target is a bytestring of the target of the symlink.
810
file_id is the inventory ID of the file, if it is to be versioned.
812
trans_id = self._new_entry(name, parent_id, file_id)
813
self.create_symlink(target, trans_id)
816
def joinpath(parent, child):
817
"""Join tree-relative paths, handling the tree root specially"""
818
if parent is None or parent == "":
821
return pathjoin(parent, child)
824
class FinalPaths(object):
825
"""Make path calculation cheap by memoizing paths.
827
The underlying tree must not be manipulated between calls, or else
828
the results will likely be incorrect.
830
def __init__(self, transform):
831
object.__init__(self)
832
self._known_paths = {}
833
self.transform = transform
835
def _determine_path(self, trans_id):
836
if trans_id == self.transform.root:
838
name = self.transform.final_name(trans_id)
839
parent_id = self.transform.final_parent(trans_id)
840
if parent_id == self.transform.root:
843
return pathjoin(self.get_path(parent_id), name)
845
def get_path(self, trans_id):
846
"""Find the final path associated with a trans_id"""
847
if trans_id not in self._known_paths:
848
self._known_paths[trans_id] = self._determine_path(trans_id)
849
return self._known_paths[trans_id]
851
def topology_sorted_ids(tree):
852
"""Determine the topological order of the ids in a tree"""
853
file_ids = list(tree)
854
file_ids.sort(key=tree.id2path)
857
def build_tree(tree, wt):
858
"""Create working tree for a branch, using a Transaction."""
860
tt = TreeTransform(wt)
862
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
863
file_ids = topology_sorted_ids(tree)
864
for file_id in file_ids:
865
entry = tree.inventory[file_id]
866
if entry.parent_id is None:
868
if entry.parent_id not in file_trans_id:
869
raise repr(entry.parent_id)
870
parent_id = file_trans_id[entry.parent_id]
871
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
876
def new_by_entry(tt, entry, parent_id, tree):
877
"""Create a new file according to its inventory entry"""
881
contents = tree.get_file(entry.file_id).readlines()
882
executable = tree.is_executable(entry.file_id)
883
return tt.new_file(name, parent_id, contents, entry.file_id,
885
elif kind == 'directory':
886
return tt.new_directory(name, parent_id, entry.file_id)
887
elif kind == 'symlink':
888
target = tree.get_symlink_target(entry.file_id)
889
return tt.new_symlink(name, parent_id, target, entry.file_id)
891
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
892
"""Create new file contents according to an inventory entry."""
893
if entry.kind == "file":
895
lines = tree.get_file(entry.file_id).readlines()
896
tt.create_file(lines, trans_id, mode_id=mode_id)
897
elif entry.kind == "symlink":
898
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
899
elif entry.kind == "directory":
900
tt.create_directory(trans_id)
902
def create_entry_executability(tt, entry, trans_id):
903
"""Set the executability of a trans_id according to an inventory entry"""
904
if entry.kind == "file":
905
tt.set_executability(entry.executable, trans_id)
908
def find_interesting(working_tree, target_tree, filenames):
909
"""Find the ids corresponding to specified filenames."""
911
interesting_ids = None
913
interesting_ids = set()
914
for tree_path in filenames:
915
for tree in (working_tree, target_tree):
917
file_id = tree.inventory.path2id(tree_path)
918
if file_id is not None:
919
interesting_ids.add(file_id)
922
raise NotVersionedError(path=tree_path)
923
return interesting_ids
926
def change_entry(tt, file_id, working_tree, target_tree,
927
trans_id_file_id, backups, trans_id):
928
"""Replace a file_id's contents with those from a target tree."""
929
e_trans_id = trans_id_file_id(file_id)
930
entry = target_tree.inventory[file_id]
931
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
937
tt.delete_contents(e_trans_id)
939
parent_trans_id = trans_id_file_id(entry.parent_id)
940
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
941
tt.unversion_file(e_trans_id)
942
e_trans_id = tt.create_path(entry.name, parent_trans_id)
943
tt.version_file(file_id, e_trans_id)
944
trans_id[file_id] = e_trans_id
945
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
946
create_entry_executability(tt, entry, e_trans_id)
949
tt.set_executability(entry.executable, e_trans_id)
950
if tt.final_name(e_trans_id) != entry.name:
953
parent_id = tt.final_parent(e_trans_id)
954
parent_file_id = tt.final_file_id(parent_id)
955
if parent_file_id != entry.parent_id:
960
parent_trans_id = trans_id_file_id(entry.parent_id)
961
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
964
def _entry_changes(file_id, entry, working_tree):
965
"""Determine in which ways the inventory entry has changed.
967
Returns booleans: has_contents, content_mod, meta_mod
968
has_contents means there are currently contents, but they differ
969
contents_mod means contents need to be modified
970
meta_mod means the metadata needs to be modified
972
cur_entry = working_tree.inventory[file_id]
974
working_kind = working_tree.kind(file_id)
977
if e.errno != errno.ENOENT:
982
if has_contents is True:
983
real_e_kind = entry.kind
984
if real_e_kind == 'root_directory':
985
real_e_kind = 'directory'
986
if real_e_kind != working_kind:
987
contents_mod, meta_mod = True, False
989
cur_entry._read_tree_state(working_tree.id2path(file_id),
991
contents_mod, meta_mod = entry.detect_changes(cur_entry)
992
cur_entry._forget_tree_state()
993
return has_contents, contents_mod, meta_mod
996
def revert(working_tree, target_tree, filenames, backups=False,
998
"""Revert a working tree's contents to those of a target tree."""
999
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1000
def interesting(file_id):
1001
return interesting_ids is None or file_id in interesting_ids
1003
tt = TreeTransform(working_tree, pb)
1006
def trans_id_file_id(file_id):
1008
return trans_id[file_id]
1010
return tt.trans_id_tree_file_id(file_id)
1012
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1014
for id_num, file_id in enumerate(sorted_interesting):
1015
pb.update("Reverting file", id_num+1, len(sorted_interesting))
1016
if file_id not in working_tree.inventory:
1017
entry = target_tree.inventory[file_id]
1018
parent_id = trans_id_file_id(entry.parent_id)
1019
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1020
trans_id[file_id] = e_trans_id
1022
change_entry(tt, file_id, working_tree, target_tree,
1023
trans_id_file_id, backups, trans_id)
1024
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1025
for id_num, file_id in enumerate(wt_interesting):
1026
pb.update("New file check", id_num+1, len(sorted_interesting))
1027
if file_id not in target_tree:
1028
tt.unversion_file(tt.trans_id_tree_file_id(file_id))
1029
raw_conflicts = resolve_conflicts(tt, pb)
1030
for line in conflicts_strings(cook_conflicts(raw_conflicts, tt)):
1038
def resolve_conflicts(tt, pb=DummyProgress()):
1039
"""Make many conflict-resolution attempts, but die if they fail"""
1040
new_conflicts = set()
1043
pb.update('Resolution pass', n+1, 10)
1044
conflicts = tt.find_conflicts()
1045
if len(conflicts) == 0:
1046
return new_conflicts
1047
new_conflicts.update(conflict_pass(tt, conflicts))
1048
raise MalformedTransform(conflicts=conflicts)
1053
def conflict_pass(tt, conflicts):
1054
"""Resolve some classes of conflicts."""
1055
new_conflicts = set()
1056
for c_type, conflict in ((c[0], c) for c in conflicts):
1057
if c_type == 'duplicate id':
1058
tt.unversion_file(conflict[1])
1059
new_conflicts.add((c_type, 'Unversioned existing file',
1060
conflict[1], conflict[2], ))
1061
elif c_type == 'duplicate':
1062
# files that were renamed take precedence
1063
new_name = tt.final_name(conflict[1])+'.moved'
1064
final_parent = tt.final_parent(conflict[1])
1065
if tt.path_changed(conflict[1]):
1066
tt.adjust_path(new_name, final_parent, conflict[2])
1067
new_conflicts.add((c_type, 'Moved existing file to',
1068
conflict[2], conflict[1]))
1070
tt.adjust_path(new_name, final_parent, conflict[1])
1071
new_conflicts.add((c_type, 'Moved existing file to',
1072
conflict[1], conflict[2]))
1073
elif c_type == 'parent loop':
1074
# break the loop by undoing one of the ops that caused the loop
1076
while not tt.path_changed(cur):
1077
cur = tt.final_parent(cur)
1078
new_conflicts.add((c_type, 'Cancelled move', cur,
1079
tt.final_parent(cur),))
1080
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1082
elif c_type == 'missing parent':
1083
trans_id = conflict[1]
1085
tt.cancel_deletion(trans_id)
1086
new_conflicts.add((c_type, 'Not deleting', trans_id))
1088
tt.create_directory(trans_id)
1089
new_conflicts.add((c_type, 'Created directory.', trans_id))
1090
elif c_type == 'unversioned parent':
1091
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1092
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1093
return new_conflicts
1095
def cook_conflicts(raw_conflicts, tt):
1096
"""Generate a list of cooked conflicts, sorted by file path"""
1098
if conflict[2] is not None:
1099
return conflict[2], conflict[0]
1100
elif len(conflict) == 6:
1101
return conflict[4], conflict[0]
1103
return None, conflict[0]
1105
return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1107
def iter_cook_conflicts(raw_conflicts, tt):
1108
cooked_conflicts = []
1110
for conflict in raw_conflicts:
1111
c_type = conflict[0]
1112
action = conflict[1]
1113
modified_path = fp.get_path(conflict[2])
1114
modified_id = tt.final_file_id(conflict[2])
1115
if len(conflict) == 3:
1116
yield c_type, action, modified_path, modified_id
1118
conflicting_path = fp.get_path(conflict[3])
1119
conflicting_id = tt.final_file_id(conflict[3])
1120
yield (c_type, action, modified_path, modified_id,
1121
conflicting_path, conflicting_id)
1124
def conflicts_strings(conflicts):
1125
"""Generate strings for the provided conflicts"""
1126
for conflict in conflicts:
1127
conflict_type = conflict[0]
1128
if conflict_type == 'text conflict':
1129
yield 'Text conflict in %s' % conflict[2]
1130
elif conflict_type == 'contents conflict':
1131
yield 'Contents conflict in %s' % conflict[2]
1132
elif conflict_type == 'path conflict':
1133
yield 'Path conflict: %s / %s' % conflict[2:]
1134
elif conflict_type == 'duplicate id':
1135
vals = (conflict[4], conflict[1], conflict[2])
1136
yield 'Conflict adding id to %s. %s %s.' % vals
1137
elif conflict_type == 'duplicate':
1138
vals = (conflict[4], conflict[1], conflict[2])
1139
yield 'Conflict adding file %s. %s %s.' % vals
1140
elif conflict_type == 'parent loop':
1141
vals = (conflict[4], conflict[2], conflict[1])
1142
yield 'Conflict moving %s into %s. %s.' % vals
1143
elif conflict_type == 'unversioned parent':
1144
vals = (conflict[2], conflict[1])
1145
yield 'Conflict adding versioned files to %s. %s.' % vals
1146
elif conflict_type == 'missing parent':
1147
vals = (conflict[2], conflict[1])
1148
yield 'Conflict adding files to %s. %s.' % vals