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, lexists,
27
from bzrlib.progress import DummyProgress, ProgressPhase
28
from bzrlib.trace import mutter, warning
32
ROOT_PARENT = "root-parent"
35
def unique_add(map, key, value):
37
raise DuplicateKey(key=key)
41
class _TransformResults(object):
42
def __init__(self, modified_paths):
44
self.modified_paths = modified_paths
47
class TreeTransform(object):
48
"""Represent a tree transformation.
50
This object is designed to support incremental generation of the transform,
53
It is easy to produce malformed transforms, but they are generally
54
harmless. Attempting to apply a malformed transform will cause an
55
exception to be raised before any modifications are made to the tree.
57
Many kinds of malformed transforms can be corrected with the
58
resolve_conflicts function. The remaining ones indicate programming error,
59
such as trying to create a file with no path.
61
Two sets of file creation methods are supplied. Convenience methods are:
66
These are composed of the low-level methods:
68
* create_file or create_directory or create_symlink
72
def __init__(self, tree, pb=DummyProgress()):
73
"""Note: a write lock is taken on the tree.
75
Use TreeTransform.finalize() to release the lock
79
self._tree.lock_write()
81
control_files = self._tree._control_files
82
self._limbodir = control_files.controlfilename('limbo')
84
os.mkdir(self._limbodir)
86
if e.errno == errno.EEXIST:
87
raise ExistingLimbo(self._limbodir)
95
self._new_contents = {}
96
self._removed_contents = set()
97
self._new_executability = {}
99
self._non_present_ids = {}
101
self._removed_id = set()
102
self._tree_path_ids = {}
103
self._tree_id_paths = {}
104
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
108
def __get_root(self):
109
return self._new_root
111
root = property(__get_root)
114
"""Release the working tree lock, if held, clean up limbo dir."""
115
if self._tree is None:
118
for trans_id, kind in self._new_contents.iteritems():
119
path = self._limbo_name(trans_id)
120
if kind == "directory":
125
os.rmdir(self._limbodir)
127
# We don't especially care *why* the dir is immortal.
128
raise ImmortalLimbo(self._limbodir)
133
def _assign_id(self):
134
"""Produce a new tranform id"""
135
new_id = "new-%s" % self._id_number
139
def create_path(self, name, parent):
140
"""Assign a transaction id to a new path"""
141
trans_id = self._assign_id()
142
unique_add(self._new_name, trans_id, name)
143
unique_add(self._new_parent, trans_id, parent)
146
def adjust_path(self, name, parent, trans_id):
147
"""Change the path that is assigned to a transaction id."""
148
if trans_id == self._new_root:
150
self._new_name[trans_id] = name
151
self._new_parent[trans_id] = parent
153
def adjust_root_path(self, name, parent):
154
"""Emulate moving the root by moving all children, instead.
156
We do this by undoing the association of root's transaction id with the
157
current tree. This allows us to create a new directory with that
158
transaction id. We unversion the root directory and version the
159
physically new directory, and hope someone versions the tree root
162
old_root = self._new_root
163
old_root_file_id = self.final_file_id(old_root)
164
# force moving all children of root
165
for child_id in self.iter_tree_children(old_root):
166
if child_id != parent:
167
self.adjust_path(self.final_name(child_id),
168
self.final_parent(child_id), child_id)
169
file_id = self.final_file_id(child_id)
170
if file_id is not None:
171
self.unversion_file(child_id)
172
self.version_file(file_id, child_id)
174
# the physical root needs a new transaction id
175
self._tree_path_ids.pop("")
176
self._tree_id_paths.pop(old_root)
177
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
178
if parent == old_root:
179
parent = self._new_root
180
self.adjust_path(name, parent, old_root)
181
self.create_directory(old_root)
182
self.version_file(old_root_file_id, old_root)
183
self.unversion_file(self._new_root)
185
def trans_id_tree_file_id(self, inventory_id):
186
"""Determine the transaction id of a working tree file.
188
This reflects only files that already exist, not ones that will be
189
added by transactions.
191
path = self._tree.inventory.id2path(inventory_id)
192
return self.trans_id_tree_path(path)
194
def trans_id_file_id(self, file_id):
195
"""Determine or set the transaction id associated with a file ID.
196
A new id is only created for file_ids that were never present. If
197
a transaction has been unversioned, it is deliberately still returned.
198
(this will likely lead to an unversioned parent conflict.)
200
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
201
return self._r_new_id[file_id]
202
elif file_id in self._tree.inventory:
203
return self.trans_id_tree_file_id(file_id)
204
elif file_id in self._non_present_ids:
205
return self._non_present_ids[file_id]
207
trans_id = self._assign_id()
208
self._non_present_ids[file_id] = trans_id
211
def canonical_path(self, path):
212
"""Get the canonical tree-relative path"""
213
# don't follow final symlinks
214
dirname, basename = os.path.split(self._tree.abspath(path))
215
dirname = os.path.realpath(dirname)
216
return self._tree.relpath(pathjoin(dirname, basename))
218
def trans_id_tree_path(self, path):
219
"""Determine (and maybe set) the transaction ID for a tree path."""
220
path = self.canonical_path(path)
221
if path not in self._tree_path_ids:
222
self._tree_path_ids[path] = self._assign_id()
223
self._tree_id_paths[self._tree_path_ids[path]] = path
224
return self._tree_path_ids[path]
226
def get_tree_parent(self, trans_id):
227
"""Determine id of the parent in the tree."""
228
path = self._tree_id_paths[trans_id]
231
return self.trans_id_tree_path(os.path.dirname(path))
233
def create_file(self, contents, trans_id, mode_id=None):
234
"""Schedule creation of a new file.
238
Contents is an iterator of strings, all of which will be written
239
to the target destination.
241
New file takes the permissions of any existing file with that id,
242
unless mode_id is specified.
244
f = file(self._limbo_name(trans_id), 'wb')
245
unique_add(self._new_contents, trans_id, 'file')
246
for segment in contents:
249
self._set_mode(trans_id, mode_id, S_ISREG)
251
def _set_mode(self, trans_id, mode_id, typefunc):
252
"""Set the mode of new file contents.
253
The mode_id is the existing file to get the mode from (often the same
254
as trans_id). The operation is only performed if there's a mode match
255
according to typefunc.
260
old_path = self._tree_id_paths[mode_id]
264
mode = os.stat(old_path).st_mode
266
if e.errno == errno.ENOENT:
271
os.chmod(self._limbo_name(trans_id), mode)
273
def create_directory(self, trans_id):
274
"""Schedule creation of a new directory.
276
See also new_directory.
278
os.mkdir(self._limbo_name(trans_id))
279
unique_add(self._new_contents, trans_id, 'directory')
281
def create_symlink(self, target, trans_id):
282
"""Schedule creation of a new symbolic link.
284
target is a bytestring.
285
See also new_symlink.
287
os.symlink(target, self._limbo_name(trans_id))
288
unique_add(self._new_contents, trans_id, 'symlink')
290
def cancel_creation(self, trans_id):
291
"""Cancel the creation of new file contents."""
292
del self._new_contents[trans_id]
293
delete_any(self._limbo_name(trans_id))
295
def delete_contents(self, trans_id):
296
"""Schedule the contents of a path entry for deletion"""
297
self.tree_kind(trans_id)
298
self._removed_contents.add(trans_id)
300
def cancel_deletion(self, trans_id):
301
"""Cancel a scheduled deletion"""
302
self._removed_contents.remove(trans_id)
304
def unversion_file(self, trans_id):
305
"""Schedule a path entry to become unversioned"""
306
self._removed_id.add(trans_id)
308
def delete_versioned(self, trans_id):
309
"""Delete and unversion a versioned file"""
310
self.delete_contents(trans_id)
311
self.unversion_file(trans_id)
313
def set_executability(self, executability, trans_id):
314
"""Schedule setting of the 'execute' bit
315
To unschedule, set to None
317
if executability is None:
318
del self._new_executability[trans_id]
320
unique_add(self._new_executability, trans_id, executability)
322
def version_file(self, file_id, trans_id):
323
"""Schedule a file to become versioned."""
324
assert file_id is not None
325
unique_add(self._new_id, trans_id, file_id)
326
unique_add(self._r_new_id, file_id, trans_id)
328
def cancel_versioning(self, trans_id):
329
"""Undo a previous versioning of a file"""
330
file_id = self._new_id[trans_id]
331
del self._new_id[trans_id]
332
del self._r_new_id[file_id]
335
"""Determine the paths of all new and changed files"""
337
fp = FinalPaths(self)
338
for id_set in (self._new_name, self._new_parent, self._new_contents,
339
self._new_id, self._new_executability):
340
new_ids.update(id_set)
341
new_paths = [(fp.get_path(t), t) for t in new_ids]
345
def tree_kind(self, trans_id):
346
"""Determine the file kind in the working tree.
348
Raises NoSuchFile if the file does not exist
350
path = self._tree_id_paths.get(trans_id)
352
raise NoSuchFile(None)
354
return file_kind(self._tree.abspath(path))
356
if e.errno != errno.ENOENT:
359
raise NoSuchFile(path)
361
def final_kind(self, trans_id):
362
"""Determine the final file kind, after any changes applied.
364
Raises NoSuchFile if the file does not exist/has no contents.
365
(It is conceivable that a path would be created without the
366
corresponding contents insertion command)
368
if trans_id in self._new_contents:
369
return self._new_contents[trans_id]
370
elif trans_id in self._removed_contents:
371
raise NoSuchFile(None)
373
return self.tree_kind(trans_id)
375
def tree_file_id(self, trans_id):
376
"""Determine the file id associated with the trans_id in the tree"""
378
path = self._tree_id_paths[trans_id]
380
# the file is a new, unversioned file, or invalid trans_id
382
# the file is old; the old id is still valid
383
if self._new_root == trans_id:
384
return self._tree.inventory.root.file_id
385
return self._tree.inventory.path2id(path)
387
def final_file_id(self, trans_id):
388
"""Determine the file id after any changes are applied, or None.
390
None indicates that the file will not be versioned after changes are
394
# there is a new id for this file
395
assert self._new_id[trans_id] is not None
396
return self._new_id[trans_id]
398
if trans_id in self._removed_id:
400
return self.tree_file_id(trans_id)
402
def inactive_file_id(self, trans_id):
403
"""Return the inactive file_id associated with a transaction id.
404
That is, the one in the tree or in non_present_ids.
405
The file_id may actually be active, too.
407
file_id = self.tree_file_id(trans_id)
408
if file_id is not None:
410
for key, value in self._non_present_ids.iteritems():
411
if value == trans_id:
414
def final_parent(self, trans_id):
415
"""Determine the parent file_id, after any changes are applied.
417
ROOT_PARENT is returned for the tree root.
420
return self._new_parent[trans_id]
422
return self.get_tree_parent(trans_id)
424
def final_name(self, trans_id):
425
"""Determine the final filename, after all changes are applied."""
427
return self._new_name[trans_id]
429
return os.path.basename(self._tree_id_paths[trans_id])
432
"""Return a map of parent: children for known parents.
434
Only new paths and parents of tree files with assigned ids are used.
437
items = list(self._new_parent.iteritems())
438
items.extend((t, self.final_parent(t)) for t in
439
self._tree_id_paths.keys())
440
for trans_id, parent_id in items:
441
if parent_id not in by_parent:
442
by_parent[parent_id] = set()
443
by_parent[parent_id].add(trans_id)
446
def path_changed(self, trans_id):
447
"""Return True if a trans_id's path has changed."""
448
return trans_id in self._new_name or trans_id in self._new_parent
450
def find_conflicts(self):
451
"""Find any violations of inventory or filesystem invariants"""
452
if self.__done is True:
453
raise ReusingTransform()
455
# ensure all children of all existent parents are known
456
# all children of non-existent parents are known, by definition.
457
self._add_tree_children()
458
by_parent = self.by_parent()
459
conflicts.extend(self._unversioned_parents(by_parent))
460
conflicts.extend(self._parent_loops())
461
conflicts.extend(self._duplicate_entries(by_parent))
462
conflicts.extend(self._duplicate_ids())
463
conflicts.extend(self._parent_type_conflicts(by_parent))
464
conflicts.extend(self._improper_versioning())
465
conflicts.extend(self._executability_conflicts())
466
conflicts.extend(self._overwrite_conflicts())
469
def _add_tree_children(self):
470
"""Add all the children of all active parents to the known paths.
472
Active parents are those which gain children, and those which are
473
removed. This is a necessary first step in detecting conflicts.
475
parents = self.by_parent().keys()
476
parents.extend([t for t in self._removed_contents if
477
self.tree_kind(t) == 'directory'])
478
for trans_id in self._removed_id:
479
file_id = self.tree_file_id(trans_id)
480
if self._tree.inventory[file_id].kind in ('directory',
482
parents.append(trans_id)
484
for parent_id in parents:
485
# ensure that all children are registered with the transaction
486
list(self.iter_tree_children(parent_id))
488
def iter_tree_children(self, parent_id):
489
"""Iterate through the entry's tree children, if any"""
491
path = self._tree_id_paths[parent_id]
495
children = os.listdir(self._tree.abspath(path))
497
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
501
for child in children:
502
childpath = joinpath(path, child)
503
if self._tree.is_control_filename(childpath):
505
yield self.trans_id_tree_path(childpath)
507
def has_named_child(self, by_parent, parent_id, name):
509
children = by_parent[parent_id]
512
for child in children:
513
if self.final_name(child) == name:
516
path = self._tree_id_paths[parent_id]
519
childpath = joinpath(path, name)
520
child_id = self._tree_path_ids.get(childpath)
522
return lexists(self._tree.abspath(childpath))
524
if tt.final_parent(child_id) != parent_id:
526
if child_id in tt._removed_contents:
527
# XXX What about dangling file-ids?
532
def _parent_loops(self):
533
"""No entry should be its own ancestor"""
535
for trans_id in self._new_parent:
538
while parent_id is not ROOT_PARENT:
540
parent_id = self.final_parent(parent_id)
541
if parent_id == trans_id:
542
conflicts.append(('parent loop', trans_id))
543
if parent_id in seen:
547
def _unversioned_parents(self, by_parent):
548
"""If parent directories are versioned, children must be versioned."""
550
for parent_id, children in by_parent.iteritems():
551
if parent_id is ROOT_PARENT:
553
if self.final_file_id(parent_id) is not None:
555
for child_id in children:
556
if self.final_file_id(child_id) is not None:
557
conflicts.append(('unversioned parent', parent_id))
561
def _improper_versioning(self):
562
"""Cannot version a file with no contents, or a bad type.
564
However, existing entries with no contents are okay.
567
for trans_id in self._new_id.iterkeys():
569
kind = self.final_kind(trans_id)
571
conflicts.append(('versioning no contents', trans_id))
573
if not InventoryEntry.versionable_kind(kind):
574
conflicts.append(('versioning bad kind', trans_id, kind))
577
def _executability_conflicts(self):
578
"""Check for bad executability changes.
580
Only versioned files may have their executability set, because
581
1. only versioned entries can have executability under windows
582
2. only files can be executable. (The execute bit on a directory
583
does not indicate searchability)
586
for trans_id in self._new_executability:
587
if self.final_file_id(trans_id) is None:
588
conflicts.append(('unversioned executability', trans_id))
591
non_file = self.final_kind(trans_id) != "file"
595
conflicts.append(('non-file executability', trans_id))
598
def _overwrite_conflicts(self):
599
"""Check for overwrites (not permitted on Win32)"""
601
for trans_id in self._new_contents:
603
self.tree_kind(trans_id)
606
if trans_id not in self._removed_contents:
607
conflicts.append(('overwrite', trans_id,
608
self.final_name(trans_id)))
611
def _duplicate_entries(self, by_parent):
612
"""No directory may have two entries with the same name."""
614
for children in by_parent.itervalues():
615
name_ids = [(self.final_name(t), t) for t in children]
619
for name, trans_id in name_ids:
620
if name == last_name:
621
conflicts.append(('duplicate', last_trans_id, trans_id,
624
kind = self.final_kind(trans_id)
627
file_id = self.final_file_id(trans_id)
628
if kind is not None or file_id is not None:
630
last_trans_id = trans_id
633
def _duplicate_ids(self):
634
"""Each inventory id may only be used once"""
636
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
638
active_tree_ids = set((f for f in self._tree.inventory if
639
f not in removed_tree_ids))
640
for trans_id, file_id in self._new_id.iteritems():
641
if file_id in active_tree_ids:
642
old_trans_id = self.trans_id_tree_file_id(file_id)
643
conflicts.append(('duplicate id', old_trans_id, trans_id))
646
def _parent_type_conflicts(self, by_parent):
647
"""parents must have directory 'contents'."""
649
for parent_id, children in by_parent.iteritems():
650
if parent_id is ROOT_PARENT:
652
if not self._any_contents(children):
654
for child in children:
656
self.final_kind(child)
660
kind = self.final_kind(parent_id)
664
conflicts.append(('missing parent', parent_id))
665
elif kind != "directory":
666
conflicts.append(('non-directory parent', parent_id))
669
def _any_contents(self, trans_ids):
670
"""Return true if any of the trans_ids, will have contents."""
671
for trans_id in trans_ids:
673
kind = self.final_kind(trans_id)
680
"""Apply all changes to the inventory and filesystem.
682
If filesystem or inventory conflicts are present, MalformedTransform
685
conflicts = self.find_conflicts()
686
if len(conflicts) != 0:
687
raise MalformedTransform(conflicts=conflicts)
689
inv = self._tree.inventory
690
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
692
child_pb.update('Apply phase', 0, 2)
693
self._apply_removals(inv, limbo_inv)
694
child_pb.update('Apply phase', 1, 2)
695
modified_paths = self._apply_insertions(inv, limbo_inv)
698
self._tree._write_inventory(inv)
701
return _TransformResults(modified_paths)
703
def _limbo_name(self, trans_id):
704
"""Generate the limbo name of a file"""
705
return pathjoin(self._limbodir, trans_id)
707
def _apply_removals(self, inv, limbo_inv):
708
"""Perform tree operations that remove directory/inventory names.
710
That is, delete files that are to be deleted, and put any files that
711
need renaming into limbo. This must be done in strict child-to-parent
714
tree_paths = list(self._tree_path_ids.iteritems())
715
tree_paths.sort(reverse=True)
716
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
718
for num, data in enumerate(tree_paths):
719
path, trans_id = data
720
child_pb.update('removing file', num, len(tree_paths))
721
full_path = self._tree.abspath(path)
722
if trans_id in self._removed_contents:
723
delete_any(full_path)
724
elif trans_id in self._new_name or trans_id in \
727
os.rename(full_path, self._limbo_name(trans_id))
729
if e.errno != errno.ENOENT:
731
if trans_id in self._removed_id:
732
if trans_id == self._new_root:
733
file_id = self._tree.inventory.root.file_id
735
file_id = self.tree_file_id(trans_id)
737
elif trans_id in self._new_name or trans_id in self._new_parent:
738
file_id = self.tree_file_id(trans_id)
739
if file_id is not None:
740
limbo_inv[trans_id] = inv[file_id]
745
def _apply_insertions(self, inv, limbo_inv):
746
"""Perform tree operations that insert directory/inventory names.
748
That is, create any files that need to be created, and restore from
749
limbo any files that needed renaming. This must be done in strict
750
parent-to-child order.
752
new_paths = self.new_paths()
754
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
756
for num, (path, trans_id) in enumerate(new_paths):
757
child_pb.update('adding file', num, len(new_paths))
759
kind = self._new_contents[trans_id]
761
kind = contents = None
762
if trans_id in self._new_contents or \
763
self.path_changed(trans_id):
764
full_path = self._tree.abspath(path)
766
os.rename(self._limbo_name(trans_id), full_path)
768
# We may be renaming a dangling inventory id
769
if e.errno != errno.ENOENT:
771
if trans_id in self._new_contents:
772
modified_paths.append(full_path)
773
del self._new_contents[trans_id]
775
if trans_id in self._new_id:
777
kind = file_kind(self._tree.abspath(path))
778
inv.add_path(path, kind, self._new_id[trans_id])
779
elif trans_id in self._new_name or trans_id in\
781
entry = limbo_inv.get(trans_id)
782
if entry is not None:
783
entry.name = self.final_name(trans_id)
784
parent_path = os.path.dirname(path)
786
self._tree.inventory.path2id(parent_path)
789
# requires files and inventory entries to be in place
790
if trans_id in self._new_executability:
791
self._set_executability(path, inv, trans_id)
794
return modified_paths
796
def _set_executability(self, path, inv, trans_id):
797
"""Set the executability of versioned files """
798
file_id = inv.path2id(path)
799
new_executability = self._new_executability[trans_id]
800
inv[file_id].executable = new_executability
801
if supports_executable():
802
abspath = self._tree.abspath(path)
803
current_mode = os.stat(abspath).st_mode
804
if new_executability:
807
to_mode = current_mode | (0100 & ~umask)
808
# Enable x-bit for others only if they can read it.
809
if current_mode & 0004:
810
to_mode |= 0001 & ~umask
811
if current_mode & 0040:
812
to_mode |= 0010 & ~umask
814
to_mode = current_mode & ~0111
815
os.chmod(abspath, to_mode)
817
def _new_entry(self, name, parent_id, file_id):
818
"""Helper function to create a new filesystem entry."""
819
trans_id = self.create_path(name, parent_id)
820
if file_id is not None:
821
self.version_file(file_id, trans_id)
824
def new_file(self, name, parent_id, contents, file_id=None,
826
"""Convenience method to create files.
828
name is the name of the file to create.
829
parent_id is the transaction id of the parent directory of the file.
830
contents is an iterator of bytestrings, which will be used to produce
832
file_id is the inventory ID of the file, if it is to be versioned.
834
trans_id = self._new_entry(name, parent_id, file_id)
835
self.create_file(contents, trans_id)
836
if executable is not None:
837
self.set_executability(executable, trans_id)
840
def new_directory(self, name, parent_id, file_id=None):
841
"""Convenience method to create directories.
843
name is the name of the directory to create.
844
parent_id is the transaction id of the parent directory of the
846
file_id is the inventory ID of the directory, if it is to be versioned.
848
trans_id = self._new_entry(name, parent_id, file_id)
849
self.create_directory(trans_id)
852
def new_symlink(self, name, parent_id, target, file_id=None):
853
"""Convenience method to create symbolic link.
855
name is the name of the symlink to create.
856
parent_id is the transaction id of the parent directory of the symlink.
857
target is a bytestring of the target of the symlink.
858
file_id is the inventory ID of the file, if it is to be versioned.
860
trans_id = self._new_entry(name, parent_id, file_id)
861
self.create_symlink(target, trans_id)
864
def joinpath(parent, child):
865
"""Join tree-relative paths, handling the tree root specially"""
866
if parent is None or parent == "":
869
return pathjoin(parent, child)
872
class FinalPaths(object):
873
"""Make path calculation cheap by memoizing paths.
875
The underlying tree must not be manipulated between calls, or else
876
the results will likely be incorrect.
878
def __init__(self, transform):
879
object.__init__(self)
880
self._known_paths = {}
881
self.transform = transform
883
def _determine_path(self, trans_id):
884
if trans_id == self.transform.root:
886
name = self.transform.final_name(trans_id)
887
parent_id = self.transform.final_parent(trans_id)
888
if parent_id == self.transform.root:
891
return pathjoin(self.get_path(parent_id), name)
893
def get_path(self, trans_id):
894
"""Find the final path associated with a trans_id"""
895
if trans_id not in self._known_paths:
896
self._known_paths[trans_id] = self._determine_path(trans_id)
897
return self._known_paths[trans_id]
899
def topology_sorted_ids(tree):
900
"""Determine the topological order of the ids in a tree"""
901
file_ids = list(tree)
902
file_ids.sort(key=tree.id2path)
905
def build_tree(tree, wt):
906
"""Create working tree for a branch, using a Transaction."""
908
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
909
pp = ProgressPhase("Build phase", 2, top_pb)
910
tt = TreeTransform(wt)
913
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
914
file_ids = topology_sorted_ids(tree)
915
pb = bzrlib.ui.ui_factory.nested_progress_bar()
917
for num, file_id in enumerate(file_ids):
918
pb.update("Building tree", num, len(file_ids))
919
entry = tree.inventory[file_id]
920
if entry.parent_id is None:
922
if entry.parent_id not in file_trans_id:
923
raise repr(entry.parent_id)
924
parent_id = file_trans_id[entry.parent_id]
925
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
935
def new_by_entry(tt, entry, parent_id, tree):
936
"""Create a new file according to its inventory entry"""
940
contents = tree.get_file(entry.file_id).readlines()
941
executable = tree.is_executable(entry.file_id)
942
return tt.new_file(name, parent_id, contents, entry.file_id,
944
elif kind == 'directory':
945
return tt.new_directory(name, parent_id, entry.file_id)
946
elif kind == 'symlink':
947
target = tree.get_symlink_target(entry.file_id)
948
return tt.new_symlink(name, parent_id, target, entry.file_id)
950
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
951
"""Create new file contents according to an inventory entry."""
952
if entry.kind == "file":
954
lines = tree.get_file(entry.file_id).readlines()
955
tt.create_file(lines, trans_id, mode_id=mode_id)
956
elif entry.kind == "symlink":
957
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
958
elif entry.kind == "directory":
959
tt.create_directory(trans_id)
961
def create_entry_executability(tt, entry, trans_id):
962
"""Set the executability of a trans_id according to an inventory entry"""
963
if entry.kind == "file":
964
tt.set_executability(entry.executable, trans_id)
967
def find_interesting(working_tree, target_tree, filenames):
968
"""Find the ids corresponding to specified filenames."""
970
interesting_ids = None
972
interesting_ids = set()
973
for tree_path in filenames:
975
for tree in (working_tree, target_tree):
976
file_id = tree.inventory.path2id(tree_path)
977
if file_id is not None:
978
interesting_ids.add(file_id)
981
raise NotVersionedError(path=tree_path)
982
return interesting_ids
985
def change_entry(tt, file_id, working_tree, target_tree,
986
trans_id_file_id, backups, trans_id, by_parent):
987
"""Replace a file_id's contents with those from a target tree."""
988
e_trans_id = trans_id_file_id(file_id)
989
entry = target_tree.inventory[file_id]
990
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
996
tt.delete_contents(e_trans_id)
998
parent_trans_id = trans_id_file_id(entry.parent_id)
999
backup_name = get_backup_name(entry, by_parent,
1000
parent_trans_id, tt)
1001
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1002
tt.unversion_file(e_trans_id)
1003
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1004
tt.version_file(file_id, e_trans_id)
1005
trans_id[file_id] = e_trans_id
1006
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1007
create_entry_executability(tt, entry, e_trans_id)
1010
tt.set_executability(entry.executable, e_trans_id)
1011
if tt.final_name(e_trans_id) != entry.name:
1014
parent_id = tt.final_parent(e_trans_id)
1015
parent_file_id = tt.final_file_id(parent_id)
1016
if parent_file_id != entry.parent_id:
1021
parent_trans_id = trans_id_file_id(entry.parent_id)
1022
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1025
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1026
"""Produce a backup-style name that appears to be available"""
1030
yield "%s.~%d~" % (entry.name, counter)
1032
for name in name_gen():
1033
if not tt.has_named_child(by_parent, parent_trans_id, name):
1036
def _entry_changes(file_id, entry, working_tree):
1037
"""Determine in which ways the inventory entry has changed.
1039
Returns booleans: has_contents, content_mod, meta_mod
1040
has_contents means there are currently contents, but they differ
1041
contents_mod means contents need to be modified
1042
meta_mod means the metadata needs to be modified
1044
cur_entry = working_tree.inventory[file_id]
1046
working_kind = working_tree.kind(file_id)
1049
if e.errno != errno.ENOENT:
1051
has_contents = False
1054
if has_contents is True:
1055
real_e_kind = entry.kind
1056
if real_e_kind == 'root_directory':
1057
real_e_kind = 'directory'
1058
if real_e_kind != working_kind:
1059
contents_mod, meta_mod = True, False
1061
cur_entry._read_tree_state(working_tree.id2path(file_id),
1063
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1064
cur_entry._forget_tree_state()
1065
return has_contents, contents_mod, meta_mod
1068
def revert(working_tree, target_tree, filenames, backups=False,
1069
pb=DummyProgress()):
1070
"""Revert a working tree's contents to those of a target tree."""
1071
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1072
def interesting(file_id):
1073
return interesting_ids is None or file_id in interesting_ids
1075
tt = TreeTransform(working_tree, pb)
1077
merge_modified = working_tree.merge_modified()
1079
def trans_id_file_id(file_id):
1081
return trans_id[file_id]
1083
return tt.trans_id_tree_file_id(file_id)
1085
pp = ProgressPhase("Revert phase", 4, pb)
1087
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1089
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1091
by_parent = tt.by_parent()
1092
for id_num, file_id in enumerate(sorted_interesting):
1093
child_pb.update("Reverting file", id_num+1,
1094
len(sorted_interesting))
1095
if file_id not in working_tree.inventory:
1096
entry = target_tree.inventory[file_id]
1097
parent_id = trans_id_file_id(entry.parent_id)
1098
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1099
trans_id[file_id] = e_trans_id
1101
backup_this = backups
1102
if file_id in merge_modified:
1104
del merge_modified[file_id]
1105
change_entry(tt, file_id, working_tree, target_tree,
1106
trans_id_file_id, backup_this, trans_id,
1111
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1112
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1114
for id_num, file_id in enumerate(wt_interesting):
1115
child_pb.update("New file check", id_num+1,
1116
len(sorted_interesting))
1117
if file_id not in target_tree:
1118
trans_id = tt.trans_id_tree_file_id(file_id)
1119
tt.unversion_file(trans_id)
1120
if file_id in merge_modified:
1121
tt.delete_contents(trans_id)
1122
del merge_modified[file_id]
1126
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1128
raw_conflicts = resolve_conflicts(tt, child_pb)
1131
conflicts = cook_conflicts(raw_conflicts, tt)
1132
for conflict in conflicts:
1136
working_tree.set_merge_modified({})
1143
def resolve_conflicts(tt, pb=DummyProgress()):
1144
"""Make many conflict-resolution attempts, but die if they fail"""
1145
new_conflicts = set()
1148
pb.update('Resolution pass', n+1, 10)
1149
conflicts = tt.find_conflicts()
1150
if len(conflicts) == 0:
1151
return new_conflicts
1152
new_conflicts.update(conflict_pass(tt, conflicts))
1153
raise MalformedTransform(conflicts=conflicts)
1158
def conflict_pass(tt, conflicts):
1159
"""Resolve some classes of conflicts."""
1160
new_conflicts = set()
1161
for c_type, conflict in ((c[0], c) for c in conflicts):
1162
if c_type == 'duplicate id':
1163
tt.unversion_file(conflict[1])
1164
new_conflicts.add((c_type, 'Unversioned existing file',
1165
conflict[1], conflict[2], ))
1166
elif c_type == 'duplicate':
1167
# files that were renamed take precedence
1168
new_name = tt.final_name(conflict[1])+'.moved'
1169
final_parent = tt.final_parent(conflict[1])
1170
if tt.path_changed(conflict[1]):
1171
tt.adjust_path(new_name, final_parent, conflict[2])
1172
new_conflicts.add((c_type, 'Moved existing file to',
1173
conflict[2], conflict[1]))
1175
tt.adjust_path(new_name, final_parent, conflict[1])
1176
new_conflicts.add((c_type, 'Moved existing file to',
1177
conflict[1], conflict[2]))
1178
elif c_type == 'parent loop':
1179
# break the loop by undoing one of the ops that caused the loop
1181
while not tt.path_changed(cur):
1182
cur = tt.final_parent(cur)
1183
new_conflicts.add((c_type, 'Cancelled move', cur,
1184
tt.final_parent(cur),))
1185
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1187
elif c_type == 'missing parent':
1188
trans_id = conflict[1]
1190
tt.cancel_deletion(trans_id)
1191
new_conflicts.add((c_type, 'Not deleting', trans_id))
1193
tt.create_directory(trans_id)
1194
new_conflicts.add((c_type, 'Created directory.', trans_id))
1195
elif c_type == 'unversioned parent':
1196
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1197
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1198
return new_conflicts
1201
def cook_conflicts(raw_conflicts, tt):
1202
"""Generate a list of cooked conflicts, sorted by file path"""
1203
from bzrlib.conflicts import Conflict
1204
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1205
return sorted(conflict_iter, key=Conflict.sort_key)
1208
def iter_cook_conflicts(raw_conflicts, tt):
1209
from bzrlib.conflicts import Conflict
1211
for conflict in raw_conflicts:
1212
c_type = conflict[0]
1213
action = conflict[1]
1214
modified_path = fp.get_path(conflict[2])
1215
modified_id = tt.final_file_id(conflict[2])
1216
if len(conflict) == 3:
1217
yield Conflict.factory(c_type, action=action, path=modified_path,
1218
file_id=modified_id)
1221
conflicting_path = fp.get_path(conflict[3])
1222
conflicting_id = tt.final_file_id(conflict[3])
1223
yield Conflict.factory(c_type, action=action, path=modified_path,
1224
file_id=modified_id,
1225
conflict_path=conflicting_path,
1226
conflict_file_id=conflicting_id)