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 _TransformResults(object):
40
def __init__(self, modified_paths):
42
self.modified_paths = modified_paths
45
class TreeTransform(object):
46
"""Represent a tree transformation.
48
This object is designed to support incremental generation of the transform,
51
It is easy to produce malformed transforms, but they are generally
52
harmless. Attempting to apply a malformed transform will cause an
53
exception to be raised before any modifications are made to the tree.
55
Many kinds of malformed transforms can be corrected with the
56
resolve_conflicts function. The remaining ones indicate programming error,
57
such as trying to create a file with no path.
59
Two sets of file creation methods are supplied. Convenience methods are:
64
These are composed of the low-level methods:
66
* create_file or create_directory or create_symlink
70
def __init__(self, tree, pb=DummyProgress()):
71
"""Note: a write lock is taken on the tree.
73
Use TreeTransform.finalize() to release the lock
77
self._tree.lock_write()
79
control_files = self._tree._control_files
80
self._limbodir = control_files.controlfilename('limbo')
82
os.mkdir(self._limbodir)
84
if e.errno == errno.EEXIST:
85
raise ExistingLimbo(self._limbodir)
93
self._new_contents = {}
94
self._removed_contents = set()
95
self._new_executability = {}
97
self._non_present_ids = {}
99
self._removed_id = set()
100
self._tree_path_ids = {}
101
self._tree_id_paths = {}
102
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
106
def __get_root(self):
107
return self._new_root
109
root = property(__get_root)
112
"""Release the working tree lock, if held, clean up limbo dir."""
113
if self._tree is None:
116
for trans_id, kind in self._new_contents.iteritems():
117
path = self._limbo_name(trans_id)
118
if kind == "directory":
123
os.rmdir(self._limbodir)
125
# We don't especially care *why* the dir is immortal.
126
raise ImmortalLimbo(self._limbodir)
131
def _assign_id(self):
132
"""Produce a new tranform id"""
133
new_id = "new-%s" % self._id_number
137
def create_path(self, name, parent):
138
"""Assign a transaction id to a new path"""
139
trans_id = self._assign_id()
140
unique_add(self._new_name, trans_id, name)
141
unique_add(self._new_parent, trans_id, parent)
144
def adjust_path(self, name, parent, trans_id):
145
"""Change the path that is assigned to a transaction id."""
146
if trans_id == self._new_root:
148
self._new_name[trans_id] = name
149
self._new_parent[trans_id] = parent
151
def adjust_root_path(self, name, parent):
152
"""Emulate moving the root by moving all children, instead.
154
We do this by undoing the association of root's transaction id with the
155
current tree. This allows us to create a new directory with that
156
transaction id. We unversion the root directory and version the
157
physically new directory, and hope someone versions the tree root
160
old_root = self._new_root
161
old_root_file_id = self.final_file_id(old_root)
162
# force moving all children of root
163
for child_id in self.iter_tree_children(old_root):
164
if child_id != parent:
165
self.adjust_path(self.final_name(child_id),
166
self.final_parent(child_id), child_id)
167
file_id = self.final_file_id(child_id)
168
if file_id is not None:
169
self.unversion_file(child_id)
170
self.version_file(file_id, child_id)
172
# the physical root needs a new transaction id
173
self._tree_path_ids.pop("")
174
self._tree_id_paths.pop(old_root)
175
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
176
if parent == old_root:
177
parent = self._new_root
178
self.adjust_path(name, parent, old_root)
179
self.create_directory(old_root)
180
self.version_file(old_root_file_id, old_root)
181
self.unversion_file(self._new_root)
183
def trans_id_tree_file_id(self, inventory_id):
184
"""Determine the transaction id of a working tree file.
186
This reflects only files that already exist, not ones that will be
187
added by transactions.
189
path = self._tree.inventory.id2path(inventory_id)
190
return self.trans_id_tree_path(path)
192
def trans_id_file_id(self, file_id):
193
"""Determine or set the transaction id associated with a file ID.
194
A new id is only created for file_ids that were never present. If
195
a transaction has been unversioned, it is deliberately still returned.
196
(this will likely lead to an unversioned parent conflict.)
198
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
199
return self._r_new_id[file_id]
200
elif file_id in self._tree.inventory:
201
return self.trans_id_tree_file_id(file_id)
202
elif file_id in self._non_present_ids:
203
return self._non_present_ids[file_id]
205
trans_id = self._assign_id()
206
self._non_present_ids[file_id] = trans_id
209
def canonical_path(self, path):
210
"""Get the canonical tree-relative path"""
211
# don't follow final symlinks
212
dirname, basename = os.path.split(self._tree.abspath(path))
213
dirname = os.path.realpath(dirname)
214
return self._tree.relpath(pathjoin(dirname, basename))
216
def trans_id_tree_path(self, path):
217
"""Determine (and maybe set) the transaction ID for a tree path."""
218
path = self.canonical_path(path)
219
if path not in self._tree_path_ids:
220
self._tree_path_ids[path] = self._assign_id()
221
self._tree_id_paths[self._tree_path_ids[path]] = path
222
return self._tree_path_ids[path]
224
def get_tree_parent(self, trans_id):
225
"""Determine id of the parent in the tree."""
226
path = self._tree_id_paths[trans_id]
229
return self.trans_id_tree_path(os.path.dirname(path))
231
def create_file(self, contents, trans_id, mode_id=None):
232
"""Schedule creation of a new file.
236
Contents is an iterator of strings, all of which will be written
237
to the target destination.
239
New file takes the permissions of any existing file with that id,
240
unless mode_id is specified.
242
f = file(self._limbo_name(trans_id), 'wb')
243
unique_add(self._new_contents, trans_id, 'file')
244
for segment in contents:
247
self._set_mode(trans_id, mode_id, S_ISREG)
249
def _set_mode(self, trans_id, mode_id, typefunc):
250
"""Set the mode of new file contents.
251
The mode_id is the existing file to get the mode from (often the same
252
as trans_id). The operation is only performed if there's a mode match
253
according to typefunc.
258
old_path = self._tree_id_paths[mode_id]
262
mode = os.stat(old_path).st_mode
264
if e.errno == errno.ENOENT:
269
os.chmod(self._limbo_name(trans_id), mode)
271
def create_directory(self, trans_id):
272
"""Schedule creation of a new directory.
274
See also new_directory.
276
os.mkdir(self._limbo_name(trans_id))
277
unique_add(self._new_contents, trans_id, 'directory')
279
def create_symlink(self, target, trans_id):
280
"""Schedule creation of a new symbolic link.
282
target is a bytestring.
283
See also new_symlink.
285
os.symlink(target, self._limbo_name(trans_id))
286
unique_add(self._new_contents, trans_id, 'symlink')
289
def delete_any(full_path):
290
"""Delete a file or directory."""
294
# We may be renaming a dangling inventory id
295
if e.errno not in (errno.EISDIR, errno.EACCES, errno.EPERM):
299
def cancel_creation(self, trans_id):
300
"""Cancel the creation of new file contents."""
301
del self._new_contents[trans_id]
302
self.delete_any(self._limbo_name(trans_id))
304
def delete_contents(self, trans_id):
305
"""Schedule the contents of a path entry for deletion"""
306
self.tree_kind(trans_id)
307
self._removed_contents.add(trans_id)
309
def cancel_deletion(self, trans_id):
310
"""Cancel a scheduled deletion"""
311
self._removed_contents.remove(trans_id)
313
def unversion_file(self, trans_id):
314
"""Schedule a path entry to become unversioned"""
315
self._removed_id.add(trans_id)
317
def delete_versioned(self, trans_id):
318
"""Delete and unversion a versioned file"""
319
self.delete_contents(trans_id)
320
self.unversion_file(trans_id)
322
def set_executability(self, executability, trans_id):
323
"""Schedule setting of the 'execute' bit
324
To unschedule, set to None
326
if executability is None:
327
del self._new_executability[trans_id]
329
unique_add(self._new_executability, trans_id, executability)
331
def version_file(self, file_id, trans_id):
332
"""Schedule a file to become versioned."""
333
assert file_id is not None
334
unique_add(self._new_id, trans_id, file_id)
335
unique_add(self._r_new_id, file_id, trans_id)
337
def cancel_versioning(self, trans_id):
338
"""Undo a previous versioning of a file"""
339
file_id = self._new_id[trans_id]
340
del self._new_id[trans_id]
341
del self._r_new_id[file_id]
344
"""Determine the paths of all new and changed files"""
346
fp = FinalPaths(self)
347
for id_set in (self._new_name, self._new_parent, self._new_contents,
348
self._new_id, self._new_executability):
349
new_ids.update(id_set)
350
new_paths = [(fp.get_path(t), t) for t in new_ids]
354
def tree_kind(self, trans_id):
355
"""Determine the file kind in the working tree.
357
Raises NoSuchFile if the file does not exist
359
path = self._tree_id_paths.get(trans_id)
361
raise NoSuchFile(None)
363
return file_kind(self._tree.abspath(path))
365
if e.errno != errno.ENOENT:
368
raise NoSuchFile(path)
370
def final_kind(self, trans_id):
371
"""Determine the final file kind, after any changes applied.
373
Raises NoSuchFile if the file does not exist/has no contents.
374
(It is conceivable that a path would be created without the
375
corresponding contents insertion command)
377
if trans_id in self._new_contents:
378
return self._new_contents[trans_id]
379
elif trans_id in self._removed_contents:
380
raise NoSuchFile(None)
382
return self.tree_kind(trans_id)
384
def tree_file_id(self, trans_id):
385
"""Determine the file id associated with the trans_id in the tree"""
387
path = self._tree_id_paths[trans_id]
389
# the file is a new, unversioned file, or invalid trans_id
391
# the file is old; the old id is still valid
392
if self._new_root == trans_id:
393
return self._tree.inventory.root.file_id
394
return self._tree.inventory.path2id(path)
396
def final_file_id(self, trans_id):
397
"""Determine the file id after any changes are applied, or None.
399
None indicates that the file will not be versioned after changes are
403
# there is a new id for this file
404
assert self._new_id[trans_id] is not None
405
return self._new_id[trans_id]
407
if trans_id in self._removed_id:
409
return self.tree_file_id(trans_id)
411
def inactive_file_id(self, trans_id):
412
"""Return the inactive file_id associated with a transaction id.
413
That is, the one in the tree or in non_present_ids.
414
The file_id may actually be active, too.
416
file_id = self.tree_file_id(trans_id)
417
if file_id is not None:
419
for key, value in self._non_present_ids.iteritems():
420
if value == trans_id:
423
def final_parent(self, trans_id):
424
"""Determine the parent file_id, after any changes are applied.
426
ROOT_PARENT is returned for the tree root.
429
return self._new_parent[trans_id]
431
return self.get_tree_parent(trans_id)
433
def final_name(self, trans_id):
434
"""Determine the final filename, after all changes are applied."""
436
return self._new_name[trans_id]
438
return os.path.basename(self._tree_id_paths[trans_id])
440
def _by_parent(self):
441
"""Return a map of parent: children for known parents.
443
Only new paths and parents of tree files with assigned ids are used.
446
items = list(self._new_parent.iteritems())
447
items.extend((t, self.final_parent(t)) for t in
448
self._tree_id_paths.keys())
449
for trans_id, parent_id in items:
450
if parent_id not in by_parent:
451
by_parent[parent_id] = set()
452
by_parent[parent_id].add(trans_id)
455
def path_changed(self, trans_id):
456
"""Return True if a trans_id's path has changed."""
457
return trans_id in self._new_name or trans_id in self._new_parent
459
def find_conflicts(self):
460
"""Find any violations of inventory or filesystem invariants"""
461
if self.__done is True:
462
raise ReusingTransform()
464
# ensure all children of all existent parents are known
465
# all children of non-existent parents are known, by definition.
466
self._add_tree_children()
467
by_parent = self._by_parent()
468
conflicts.extend(self._unversioned_parents(by_parent))
469
conflicts.extend(self._parent_loops())
470
conflicts.extend(self._duplicate_entries(by_parent))
471
conflicts.extend(self._duplicate_ids())
472
conflicts.extend(self._parent_type_conflicts(by_parent))
473
conflicts.extend(self._improper_versioning())
474
conflicts.extend(self._executability_conflicts())
475
conflicts.extend(self._overwrite_conflicts())
478
def _add_tree_children(self):
479
"""Add all the children of all active parents to the known paths.
481
Active parents are those which gain children, and those which are
482
removed. This is a necessary first step in detecting conflicts.
484
parents = self._by_parent().keys()
485
parents.extend([t for t in self._removed_contents if
486
self.tree_kind(t) == 'directory'])
487
for trans_id in self._removed_id:
488
file_id = self.tree_file_id(trans_id)
489
if self._tree.inventory[file_id].kind in ('directory',
491
parents.append(trans_id)
493
for parent_id in parents:
494
# ensure that all children are registered with the transaction
495
list(self.iter_tree_children(parent_id))
497
def iter_tree_children(self, parent_id):
498
"""Iterate through the entry's tree children, if any"""
500
path = self._tree_id_paths[parent_id]
504
children = os.listdir(self._tree.abspath(path))
506
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
510
for child in children:
511
childpath = joinpath(path, child)
512
if self._tree.is_control_filename(childpath):
514
yield self.trans_id_tree_path(childpath)
516
def _parent_loops(self):
517
"""No entry should be its own ancestor"""
519
for trans_id in self._new_parent:
522
while parent_id is not ROOT_PARENT:
524
parent_id = self.final_parent(parent_id)
525
if parent_id == trans_id:
526
conflicts.append(('parent loop', trans_id))
527
if parent_id in seen:
531
def _unversioned_parents(self, by_parent):
532
"""If parent directories are versioned, children must be versioned."""
534
for parent_id, children in by_parent.iteritems():
535
if parent_id is ROOT_PARENT:
537
if self.final_file_id(parent_id) is not None:
539
for child_id in children:
540
if self.final_file_id(child_id) is not None:
541
conflicts.append(('unversioned parent', parent_id))
545
def _improper_versioning(self):
546
"""Cannot version a file with no contents, or a bad type.
548
However, existing entries with no contents are okay.
551
for trans_id in self._new_id.iterkeys():
553
kind = self.final_kind(trans_id)
555
conflicts.append(('versioning no contents', trans_id))
557
if not InventoryEntry.versionable_kind(kind):
558
conflicts.append(('versioning bad kind', trans_id, kind))
561
def _executability_conflicts(self):
562
"""Check for bad executability changes.
564
Only versioned files may have their executability set, because
565
1. only versioned entries can have executability under windows
566
2. only files can be executable. (The execute bit on a directory
567
does not indicate searchability)
570
for trans_id in self._new_executability:
571
if self.final_file_id(trans_id) is None:
572
conflicts.append(('unversioned executability', trans_id))
575
non_file = self.final_kind(trans_id) != "file"
579
conflicts.append(('non-file executability', trans_id))
582
def _overwrite_conflicts(self):
583
"""Check for overwrites (not permitted on Win32)"""
585
for trans_id in self._new_contents:
587
self.tree_kind(trans_id)
590
if trans_id not in self._removed_contents:
591
conflicts.append(('overwrite', trans_id,
592
self.final_name(trans_id)))
595
def _duplicate_entries(self, by_parent):
596
"""No directory may have two entries with the same name."""
598
for children in by_parent.itervalues():
599
name_ids = [(self.final_name(t), t) for t in children]
603
for name, trans_id in name_ids:
604
if name == last_name:
605
conflicts.append(('duplicate', last_trans_id, trans_id,
608
last_trans_id = trans_id
611
def _duplicate_ids(self):
612
"""Each inventory id may only be used once"""
614
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
616
active_tree_ids = set((f for f in self._tree.inventory if
617
f not in removed_tree_ids))
618
for trans_id, file_id in self._new_id.iteritems():
619
if file_id in active_tree_ids:
620
old_trans_id = self.trans_id_tree_file_id(file_id)
621
conflicts.append(('duplicate id', old_trans_id, trans_id))
624
def _parent_type_conflicts(self, by_parent):
625
"""parents must have directory 'contents'."""
627
for parent_id, children in by_parent.iteritems():
628
if parent_id is ROOT_PARENT:
630
if not self._any_contents(children):
632
for child in children:
634
self.final_kind(child)
638
kind = self.final_kind(parent_id)
642
conflicts.append(('missing parent', parent_id))
643
elif kind != "directory":
644
conflicts.append(('non-directory parent', parent_id))
647
def _any_contents(self, trans_ids):
648
"""Return true if any of the trans_ids, will have contents."""
649
for trans_id in trans_ids:
651
kind = self.final_kind(trans_id)
658
"""Apply all changes to the inventory and filesystem.
660
If filesystem or inventory conflicts are present, MalformedTransform
663
conflicts = self.find_conflicts()
664
if len(conflicts) != 0:
665
raise MalformedTransform(conflicts=conflicts)
667
inv = self._tree.inventory
668
self._apply_removals(inv, limbo_inv)
669
modified_paths = self._apply_insertions(inv, limbo_inv)
670
self._tree._write_inventory(inv)
673
return _TransformResults(modified_paths)
675
def _limbo_name(self, trans_id):
676
"""Generate the limbo name of a file"""
677
return pathjoin(self._limbodir, trans_id)
679
def _apply_removals(self, inv, limbo_inv):
680
"""Perform tree operations that remove directory/inventory names.
682
That is, delete files that are to be deleted, and put any files that
683
need renaming into limbo. This must be done in strict child-to-parent
686
tree_paths = list(self._tree_path_ids.iteritems())
687
tree_paths.sort(reverse=True)
688
for num, data in enumerate(tree_paths):
689
path, trans_id = data
690
self._pb.update('removing file', num+1, len(tree_paths))
691
full_path = self._tree.abspath(path)
692
if trans_id in self._removed_contents:
693
self.delete_any(full_path)
694
elif trans_id in self._new_name or trans_id in self._new_parent:
696
os.rename(full_path, self._limbo_name(trans_id))
698
if e.errno != errno.ENOENT:
700
if trans_id in self._removed_id:
701
if trans_id == self._new_root:
702
file_id = self._tree.inventory.root.file_id
704
file_id = self.tree_file_id(trans_id)
706
elif trans_id in self._new_name or trans_id in self._new_parent:
707
file_id = self.tree_file_id(trans_id)
708
if file_id is not None:
709
limbo_inv[trans_id] = inv[file_id]
713
def _apply_insertions(self, inv, limbo_inv):
714
"""Perform tree operations that insert directory/inventory names.
716
That is, create any files that need to be created, and restore from
717
limbo any files that needed renaming. This must be done in strict
718
parent-to-child order.
720
new_paths = self.new_paths()
722
for num, (path, trans_id) in enumerate(new_paths):
723
self._pb.update('adding file', num+1, len(new_paths))
725
kind = self._new_contents[trans_id]
727
kind = contents = None
728
if trans_id in self._new_contents or self.path_changed(trans_id):
729
full_path = self._tree.abspath(path)
731
os.rename(self._limbo_name(trans_id), full_path)
733
# We may be renaming a dangling inventory id
734
if e.errno != errno.ENOENT:
736
if trans_id in self._new_contents:
737
modified_paths.append(full_path)
738
del self._new_contents[trans_id]
740
if trans_id in self._new_id:
742
kind = file_kind(self._tree.abspath(path))
743
inv.add_path(path, kind, self._new_id[trans_id])
744
elif trans_id in self._new_name or trans_id in self._new_parent:
745
entry = limbo_inv.get(trans_id)
746
if entry is not None:
747
entry.name = self.final_name(trans_id)
748
parent_path = os.path.dirname(path)
749
entry.parent_id = self._tree.inventory.path2id(parent_path)
752
# requires files and inventory entries to be in place
753
if trans_id in self._new_executability:
754
self._set_executability(path, inv, trans_id)
756
return modified_paths
758
def _set_executability(self, path, inv, trans_id):
759
"""Set the executability of versioned files """
760
file_id = inv.path2id(path)
761
new_executability = self._new_executability[trans_id]
762
inv[file_id].executable = new_executability
763
if supports_executable():
764
abspath = self._tree.abspath(path)
765
current_mode = os.stat(abspath).st_mode
766
if new_executability:
769
to_mode = current_mode | (0100 & ~umask)
770
# Enable x-bit for others only if they can read it.
771
if current_mode & 0004:
772
to_mode |= 0001 & ~umask
773
if current_mode & 0040:
774
to_mode |= 0010 & ~umask
776
to_mode = current_mode & ~0111
777
os.chmod(abspath, to_mode)
779
def _new_entry(self, name, parent_id, file_id):
780
"""Helper function to create a new filesystem entry."""
781
trans_id = self.create_path(name, parent_id)
782
if file_id is not None:
783
self.version_file(file_id, trans_id)
786
def new_file(self, name, parent_id, contents, file_id=None,
788
"""Convenience method to create files.
790
name is the name of the file to create.
791
parent_id is the transaction id of the parent directory of the file.
792
contents is an iterator of bytestrings, which will be used to produce
794
file_id is the inventory ID of the file, if it is to be versioned.
796
trans_id = self._new_entry(name, parent_id, file_id)
797
self.create_file(contents, trans_id)
798
if executable is not None:
799
self.set_executability(executable, trans_id)
802
def new_directory(self, name, parent_id, file_id=None):
803
"""Convenience method to create directories.
805
name is the name of the directory to create.
806
parent_id is the transaction id of the parent directory of the
808
file_id is the inventory ID of the directory, if it is to be versioned.
810
trans_id = self._new_entry(name, parent_id, file_id)
811
self.create_directory(trans_id)
814
def new_symlink(self, name, parent_id, target, file_id=None):
815
"""Convenience method to create symbolic link.
817
name is the name of the symlink to create.
818
parent_id is the transaction id of the parent directory of the symlink.
819
target is a bytestring of the target of the symlink.
820
file_id is the inventory ID of the file, if it is to be versioned.
822
trans_id = self._new_entry(name, parent_id, file_id)
823
self.create_symlink(target, trans_id)
826
def joinpath(parent, child):
827
"""Join tree-relative paths, handling the tree root specially"""
828
if parent is None or parent == "":
831
return pathjoin(parent, child)
834
class FinalPaths(object):
835
"""Make path calculation cheap by memoizing paths.
837
The underlying tree must not be manipulated between calls, or else
838
the results will likely be incorrect.
840
def __init__(self, transform):
841
object.__init__(self)
842
self._known_paths = {}
843
self.transform = transform
845
def _determine_path(self, trans_id):
846
if trans_id == self.transform.root:
848
name = self.transform.final_name(trans_id)
849
parent_id = self.transform.final_parent(trans_id)
850
if parent_id == self.transform.root:
853
return pathjoin(self.get_path(parent_id), name)
855
def get_path(self, trans_id):
856
"""Find the final path associated with a trans_id"""
857
if trans_id not in self._known_paths:
858
self._known_paths[trans_id] = self._determine_path(trans_id)
859
return self._known_paths[trans_id]
861
def topology_sorted_ids(tree):
862
"""Determine the topological order of the ids in a tree"""
863
file_ids = list(tree)
864
file_ids.sort(key=tree.id2path)
867
def build_tree(tree, wt):
868
"""Create working tree for a branch, using a Transaction."""
870
tt = TreeTransform(wt)
872
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
873
file_ids = topology_sorted_ids(tree)
874
for file_id in file_ids:
875
entry = tree.inventory[file_id]
876
if entry.parent_id is None:
878
if entry.parent_id not in file_trans_id:
879
raise repr(entry.parent_id)
880
parent_id = file_trans_id[entry.parent_id]
881
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
886
def new_by_entry(tt, entry, parent_id, tree):
887
"""Create a new file according to its inventory entry"""
891
contents = tree.get_file(entry.file_id).readlines()
892
executable = tree.is_executable(entry.file_id)
893
return tt.new_file(name, parent_id, contents, entry.file_id,
895
elif kind == 'directory':
896
return tt.new_directory(name, parent_id, entry.file_id)
897
elif kind == 'symlink':
898
target = tree.get_symlink_target(entry.file_id)
899
return tt.new_symlink(name, parent_id, target, entry.file_id)
901
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
902
"""Create new file contents according to an inventory entry."""
903
if entry.kind == "file":
905
lines = tree.get_file(entry.file_id).readlines()
906
tt.create_file(lines, trans_id, mode_id=mode_id)
907
elif entry.kind == "symlink":
908
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
909
elif entry.kind == "directory":
910
tt.create_directory(trans_id)
912
def create_entry_executability(tt, entry, trans_id):
913
"""Set the executability of a trans_id according to an inventory entry"""
914
if entry.kind == "file":
915
tt.set_executability(entry.executable, trans_id)
918
def find_interesting(working_tree, target_tree, filenames):
919
"""Find the ids corresponding to specified filenames."""
921
interesting_ids = None
923
interesting_ids = set()
924
for tree_path in filenames:
925
for tree in (working_tree, target_tree):
927
file_id = tree.inventory.path2id(tree_path)
928
if file_id is not None:
929
interesting_ids.add(file_id)
932
raise NotVersionedError(path=tree_path)
933
return interesting_ids
936
def change_entry(tt, file_id, working_tree, target_tree,
937
trans_id_file_id, backups, trans_id):
938
"""Replace a file_id's contents with those from a target tree."""
939
e_trans_id = trans_id_file_id(file_id)
940
entry = target_tree.inventory[file_id]
941
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
947
tt.delete_contents(e_trans_id)
949
parent_trans_id = trans_id_file_id(entry.parent_id)
950
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
951
tt.unversion_file(e_trans_id)
952
e_trans_id = tt.create_path(entry.name, parent_trans_id)
953
tt.version_file(file_id, e_trans_id)
954
trans_id[file_id] = e_trans_id
955
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
956
create_entry_executability(tt, entry, e_trans_id)
959
tt.set_executability(entry.executable, e_trans_id)
960
if tt.final_name(e_trans_id) != entry.name:
963
parent_id = tt.final_parent(e_trans_id)
964
parent_file_id = tt.final_file_id(parent_id)
965
if parent_file_id != entry.parent_id:
970
parent_trans_id = trans_id_file_id(entry.parent_id)
971
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
974
def _entry_changes(file_id, entry, working_tree):
975
"""Determine in which ways the inventory entry has changed.
977
Returns booleans: has_contents, content_mod, meta_mod
978
has_contents means there are currently contents, but they differ
979
contents_mod means contents need to be modified
980
meta_mod means the metadata needs to be modified
982
cur_entry = working_tree.inventory[file_id]
984
working_kind = working_tree.kind(file_id)
987
if e.errno != errno.ENOENT:
992
if has_contents is True:
993
real_e_kind = entry.kind
994
if real_e_kind == 'root_directory':
995
real_e_kind = 'directory'
996
if real_e_kind != working_kind:
997
contents_mod, meta_mod = True, False
999
cur_entry._read_tree_state(working_tree.id2path(file_id),
1001
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1002
cur_entry._forget_tree_state()
1003
return has_contents, contents_mod, meta_mod
1006
def revert(working_tree, target_tree, filenames, backups=False,
1007
pb=DummyProgress()):
1008
"""Revert a working tree's contents to those of a target tree."""
1009
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1010
def interesting(file_id):
1011
return interesting_ids is None or file_id in interesting_ids
1013
tt = TreeTransform(working_tree, pb)
1014
merge_modified = working_tree.merge_modified()
1017
def trans_id_file_id(file_id):
1019
return trans_id[file_id]
1021
return tt.trans_id_tree_file_id(file_id)
1023
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1025
for id_num, file_id in enumerate(sorted_interesting):
1026
pb.update("Reverting file", id_num+1, len(sorted_interesting))
1027
if file_id not in working_tree.inventory:
1028
entry = target_tree.inventory[file_id]
1029
parent_id = trans_id_file_id(entry.parent_id)
1030
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1031
trans_id[file_id] = e_trans_id
1033
backup_this = backups
1034
if file_id in merge_modified:
1036
del merge_modified[file_id]
1037
change_entry(tt, file_id, working_tree, target_tree,
1038
trans_id_file_id, backup_this, trans_id)
1039
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1040
for id_num, file_id in enumerate(wt_interesting):
1041
pb.update("New file check", id_num+1, len(sorted_interesting))
1042
if file_id not in target_tree:
1043
trans_id = tt.trans_id_tree_file_id(file_id)
1044
tt.unversion_file(trans_id)
1045
if file_id in merge_modified:
1046
tt.delete_contents(trans_id)
1047
del merge_modified[file_id]
1048
raw_conflicts = resolve_conflicts(tt, pb)
1049
for line in conflicts_strings(cook_conflicts(raw_conflicts, tt)):
1052
working_tree.set_merge_modified({})
1058
def resolve_conflicts(tt, pb=DummyProgress()):
1059
"""Make many conflict-resolution attempts, but die if they fail"""
1060
new_conflicts = set()
1063
pb.update('Resolution pass', n+1, 10)
1064
conflicts = tt.find_conflicts()
1065
if len(conflicts) == 0:
1066
return new_conflicts
1067
new_conflicts.update(conflict_pass(tt, conflicts))
1068
raise MalformedTransform(conflicts=conflicts)
1073
def conflict_pass(tt, conflicts):
1074
"""Resolve some classes of conflicts."""
1075
new_conflicts = set()
1076
for c_type, conflict in ((c[0], c) for c in conflicts):
1077
if c_type == 'duplicate id':
1078
tt.unversion_file(conflict[1])
1079
new_conflicts.add((c_type, 'Unversioned existing file',
1080
conflict[1], conflict[2], ))
1081
elif c_type == 'duplicate':
1082
# files that were renamed take precedence
1083
new_name = tt.final_name(conflict[1])+'.moved'
1084
final_parent = tt.final_parent(conflict[1])
1085
if tt.path_changed(conflict[1]):
1086
tt.adjust_path(new_name, final_parent, conflict[2])
1087
new_conflicts.add((c_type, 'Moved existing file to',
1088
conflict[2], conflict[1]))
1090
tt.adjust_path(new_name, final_parent, conflict[1])
1091
new_conflicts.add((c_type, 'Moved existing file to',
1092
conflict[1], conflict[2]))
1093
elif c_type == 'parent loop':
1094
# break the loop by undoing one of the ops that caused the loop
1096
while not tt.path_changed(cur):
1097
cur = tt.final_parent(cur)
1098
new_conflicts.add((c_type, 'Cancelled move', cur,
1099
tt.final_parent(cur),))
1100
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1102
elif c_type == 'missing parent':
1103
trans_id = conflict[1]
1105
tt.cancel_deletion(trans_id)
1106
new_conflicts.add((c_type, 'Not deleting', trans_id))
1108
tt.create_directory(trans_id)
1109
new_conflicts.add((c_type, 'Created directory.', trans_id))
1110
elif c_type == 'unversioned parent':
1111
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1112
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1113
return new_conflicts
1115
def cook_conflicts(raw_conflicts, tt):
1116
"""Generate a list of cooked conflicts, sorted by file path"""
1118
if conflict[2] is not None:
1119
return conflict[2], conflict[0]
1120
elif len(conflict) == 6:
1121
return conflict[4], conflict[0]
1123
return None, conflict[0]
1125
return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1127
def iter_cook_conflicts(raw_conflicts, tt):
1128
cooked_conflicts = []
1130
for conflict in raw_conflicts:
1131
c_type = conflict[0]
1132
action = conflict[1]
1133
modified_path = fp.get_path(conflict[2])
1134
modified_id = tt.final_file_id(conflict[2])
1135
if len(conflict) == 3:
1136
yield c_type, action, modified_path, modified_id
1138
conflicting_path = fp.get_path(conflict[3])
1139
conflicting_id = tt.final_file_id(conflict[3])
1140
yield (c_type, action, modified_path, modified_id,
1141
conflicting_path, conflicting_id)
1144
def conflicts_strings(conflicts):
1145
"""Generate strings for the provided conflicts"""
1146
for conflict in conflicts:
1147
conflict_type = conflict[0]
1148
if conflict_type == 'text conflict':
1149
yield 'Text conflict in %s' % conflict[2]
1150
elif conflict_type == 'contents conflict':
1151
yield 'Contents conflict in %s' % conflict[2]
1152
elif conflict_type == 'path conflict':
1153
yield 'Path conflict: %s / %s' % conflict[2:]
1154
elif conflict_type == 'duplicate id':
1155
vals = (conflict[4], conflict[1], conflict[2])
1156
yield 'Conflict adding id to %s. %s %s.' % vals
1157
elif conflict_type == 'duplicate':
1158
vals = (conflict[4], conflict[1], conflict[2])
1159
yield 'Conflict adding file %s. %s %s.' % vals
1160
elif conflict_type == 'parent loop':
1161
vals = (conflict[4], conflict[2], conflict[1])
1162
yield 'Conflict moving %s into %s. %s.' % vals
1163
elif conflict_type == 'unversioned parent':
1164
vals = (conflict[2], conflict[1])
1165
yield 'Conflict adding versioned files to %s. %s.' % vals
1166
elif conflict_type == 'missing parent':
1167
vals = (conflict[2], conflict[1])
1168
yield 'Conflict adding files to %s. %s.' % vals