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 = {}
105
# Cache of realpath results, to speed up canonical_path
107
# Cache of relpath results, to speed up canonical_path
108
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
112
def __get_root(self):
113
return self._new_root
115
root = property(__get_root)
118
"""Release the working tree lock, if held, clean up limbo dir."""
119
if self._tree is None:
122
for trans_id, kind in self._new_contents.iteritems():
123
path = self._limbo_name(trans_id)
124
if kind == "directory":
129
os.rmdir(self._limbodir)
131
# We don't especially care *why* the dir is immortal.
132
raise ImmortalLimbo(self._limbodir)
137
def _assign_id(self):
138
"""Produce a new tranform id"""
139
new_id = "new-%s" % self._id_number
143
def create_path(self, name, parent):
144
"""Assign a transaction id to a new path"""
145
trans_id = self._assign_id()
146
unique_add(self._new_name, trans_id, name)
147
unique_add(self._new_parent, trans_id, parent)
150
def adjust_path(self, name, parent, trans_id):
151
"""Change the path that is assigned to a transaction id."""
152
if trans_id == self._new_root:
154
self._new_name[trans_id] = name
155
self._new_parent[trans_id] = parent
157
def adjust_root_path(self, name, parent):
158
"""Emulate moving the root by moving all children, instead.
160
We do this by undoing the association of root's transaction id with the
161
current tree. This allows us to create a new directory with that
162
transaction id. We unversion the root directory and version the
163
physically new directory, and hope someone versions the tree root
166
old_root = self._new_root
167
old_root_file_id = self.final_file_id(old_root)
168
# force moving all children of root
169
for child_id in self.iter_tree_children(old_root):
170
if child_id != parent:
171
self.adjust_path(self.final_name(child_id),
172
self.final_parent(child_id), child_id)
173
file_id = self.final_file_id(child_id)
174
if file_id is not None:
175
self.unversion_file(child_id)
176
self.version_file(file_id, child_id)
178
# the physical root needs a new transaction id
179
self._tree_path_ids.pop("")
180
self._tree_id_paths.pop(old_root)
181
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
182
if parent == old_root:
183
parent = self._new_root
184
self.adjust_path(name, parent, old_root)
185
self.create_directory(old_root)
186
self.version_file(old_root_file_id, old_root)
187
self.unversion_file(self._new_root)
189
def trans_id_tree_file_id(self, inventory_id):
190
"""Determine the transaction id of a working tree file.
192
This reflects only files that already exist, not ones that will be
193
added by transactions.
195
path = self._tree.inventory.id2path(inventory_id)
196
return self.trans_id_tree_path(path)
198
def trans_id_file_id(self, file_id):
199
"""Determine or set the transaction id associated with a file ID.
200
A new id is only created for file_ids that were never present. If
201
a transaction has been unversioned, it is deliberately still returned.
202
(this will likely lead to an unversioned parent conflict.)
204
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
205
return self._r_new_id[file_id]
206
elif file_id in self._tree.inventory:
207
return self.trans_id_tree_file_id(file_id)
208
elif file_id in self._non_present_ids:
209
return self._non_present_ids[file_id]
211
trans_id = self._assign_id()
212
self._non_present_ids[file_id] = trans_id
215
def canonical_path(self, path):
216
"""Get the canonical tree-relative path"""
217
# don't follow final symlinks
218
abs = self._tree.abspath(path)
219
if abs in self._relpaths:
220
return self._relpaths[abs]
221
dirname, basename = os.path.split(abs)
222
if dirname not in self._realpaths:
223
self._realpaths[dirname] = os.path.realpath(dirname)
224
dirname = self._realpaths[dirname]
225
abs = pathjoin(dirname, basename)
226
if dirname in self._relpaths:
227
relpath = pathjoin(self._relpaths[dirname], basename)
228
relpath = relpath.rstrip('/\\')
230
relpath = self._tree.relpath(abs)
231
self._relpaths[abs] = relpath
234
def trans_id_tree_path(self, path):
235
"""Determine (and maybe set) the transaction ID for a tree path."""
236
path = self.canonical_path(path)
237
if path not in self._tree_path_ids:
238
self._tree_path_ids[path] = self._assign_id()
239
self._tree_id_paths[self._tree_path_ids[path]] = path
240
return self._tree_path_ids[path]
242
def get_tree_parent(self, trans_id):
243
"""Determine id of the parent in the tree."""
244
path = self._tree_id_paths[trans_id]
247
return self.trans_id_tree_path(os.path.dirname(path))
249
def create_file(self, contents, trans_id, mode_id=None):
250
"""Schedule creation of a new file.
254
Contents is an iterator of strings, all of which will be written
255
to the target destination.
257
New file takes the permissions of any existing file with that id,
258
unless mode_id is specified.
260
f = file(self._limbo_name(trans_id), 'wb')
261
unique_add(self._new_contents, trans_id, 'file')
262
for segment in contents:
265
self._set_mode(trans_id, mode_id, S_ISREG)
267
def _set_mode(self, trans_id, mode_id, typefunc):
268
"""Set the mode of new file contents.
269
The mode_id is the existing file to get the mode from (often the same
270
as trans_id). The operation is only performed if there's a mode match
271
according to typefunc.
276
old_path = self._tree_id_paths[mode_id]
280
mode = os.stat(old_path).st_mode
282
if e.errno == errno.ENOENT:
287
os.chmod(self._limbo_name(trans_id), mode)
289
def create_directory(self, trans_id):
290
"""Schedule creation of a new directory.
292
See also new_directory.
294
os.mkdir(self._limbo_name(trans_id))
295
unique_add(self._new_contents, trans_id, 'directory')
297
def create_symlink(self, target, trans_id):
298
"""Schedule creation of a new symbolic link.
300
target is a bytestring.
301
See also new_symlink.
303
os.symlink(target, self._limbo_name(trans_id))
304
unique_add(self._new_contents, trans_id, 'symlink')
306
def cancel_creation(self, trans_id):
307
"""Cancel the creation of new file contents."""
308
del self._new_contents[trans_id]
309
delete_any(self._limbo_name(trans_id))
311
def delete_contents(self, trans_id):
312
"""Schedule the contents of a path entry for deletion"""
313
self.tree_kind(trans_id)
314
self._removed_contents.add(trans_id)
316
def cancel_deletion(self, trans_id):
317
"""Cancel a scheduled deletion"""
318
self._removed_contents.remove(trans_id)
320
def unversion_file(self, trans_id):
321
"""Schedule a path entry to become unversioned"""
322
self._removed_id.add(trans_id)
324
def delete_versioned(self, trans_id):
325
"""Delete and unversion a versioned file"""
326
self.delete_contents(trans_id)
327
self.unversion_file(trans_id)
329
def set_executability(self, executability, trans_id):
330
"""Schedule setting of the 'execute' bit
331
To unschedule, set to None
333
if executability is None:
334
del self._new_executability[trans_id]
336
unique_add(self._new_executability, trans_id, executability)
338
def version_file(self, file_id, trans_id):
339
"""Schedule a file to become versioned."""
340
assert file_id is not None
341
unique_add(self._new_id, trans_id, file_id)
342
unique_add(self._r_new_id, file_id, trans_id)
344
def cancel_versioning(self, trans_id):
345
"""Undo a previous versioning of a file"""
346
file_id = self._new_id[trans_id]
347
del self._new_id[trans_id]
348
del self._r_new_id[file_id]
351
"""Determine the paths of all new and changed files"""
353
fp = FinalPaths(self)
354
for id_set in (self._new_name, self._new_parent, self._new_contents,
355
self._new_id, self._new_executability):
356
new_ids.update(id_set)
357
new_paths = [(fp.get_path(t), t) for t in new_ids]
361
def tree_kind(self, trans_id):
362
"""Determine the file kind in the working tree.
364
Raises NoSuchFile if the file does not exist
366
path = self._tree_id_paths.get(trans_id)
368
raise NoSuchFile(None)
370
return file_kind(self._tree.abspath(path))
372
if e.errno != errno.ENOENT:
375
raise NoSuchFile(path)
377
def final_kind(self, trans_id):
378
"""Determine the final file kind, after any changes applied.
380
Raises NoSuchFile if the file does not exist/has no contents.
381
(It is conceivable that a path would be created without the
382
corresponding contents insertion command)
384
if trans_id in self._new_contents:
385
return self._new_contents[trans_id]
386
elif trans_id in self._removed_contents:
387
raise NoSuchFile(None)
389
return self.tree_kind(trans_id)
391
def tree_file_id(self, trans_id):
392
"""Determine the file id associated with the trans_id in the tree"""
394
path = self._tree_id_paths[trans_id]
396
# the file is a new, unversioned file, or invalid trans_id
398
# the file is old; the old id is still valid
399
if self._new_root == trans_id:
400
return self._tree.inventory.root.file_id
401
return self._tree.inventory.path2id(path)
403
def final_file_id(self, trans_id):
404
"""Determine the file id after any changes are applied, or None.
406
None indicates that the file will not be versioned after changes are
410
# there is a new id for this file
411
assert self._new_id[trans_id] is not None
412
return self._new_id[trans_id]
414
if trans_id in self._removed_id:
416
return self.tree_file_id(trans_id)
418
def inactive_file_id(self, trans_id):
419
"""Return the inactive file_id associated with a transaction id.
420
That is, the one in the tree or in non_present_ids.
421
The file_id may actually be active, too.
423
file_id = self.tree_file_id(trans_id)
424
if file_id is not None:
426
for key, value in self._non_present_ids.iteritems():
427
if value == trans_id:
430
def final_parent(self, trans_id):
431
"""Determine the parent file_id, after any changes are applied.
433
ROOT_PARENT is returned for the tree root.
436
return self._new_parent[trans_id]
438
return self.get_tree_parent(trans_id)
440
def final_name(self, trans_id):
441
"""Determine the final filename, after all changes are applied."""
443
return self._new_name[trans_id]
445
return os.path.basename(self._tree_id_paths[trans_id])
448
"""Return a map of parent: children for known parents.
450
Only new paths and parents of tree files with assigned ids are used.
453
items = list(self._new_parent.iteritems())
454
items.extend((t, self.final_parent(t)) for t in
455
self._tree_id_paths.keys())
456
for trans_id, parent_id in items:
457
if parent_id not in by_parent:
458
by_parent[parent_id] = set()
459
by_parent[parent_id].add(trans_id)
462
def path_changed(self, trans_id):
463
"""Return True if a trans_id's path has changed."""
464
return trans_id in self._new_name or trans_id in self._new_parent
466
def find_conflicts(self):
467
"""Find any violations of inventory or filesystem invariants"""
468
if self.__done is True:
469
raise ReusingTransform()
471
# ensure all children of all existent parents are known
472
# all children of non-existent parents are known, by definition.
473
self._add_tree_children()
474
by_parent = self.by_parent()
475
conflicts.extend(self._unversioned_parents(by_parent))
476
conflicts.extend(self._parent_loops())
477
conflicts.extend(self._duplicate_entries(by_parent))
478
conflicts.extend(self._duplicate_ids())
479
conflicts.extend(self._parent_type_conflicts(by_parent))
480
conflicts.extend(self._improper_versioning())
481
conflicts.extend(self._executability_conflicts())
482
conflicts.extend(self._overwrite_conflicts())
485
def _add_tree_children(self):
486
"""Add all the children of all active parents to the known paths.
488
Active parents are those which gain children, and those which are
489
removed. This is a necessary first step in detecting conflicts.
491
parents = self.by_parent().keys()
492
parents.extend([t for t in self._removed_contents if
493
self.tree_kind(t) == 'directory'])
494
for trans_id in self._removed_id:
495
file_id = self.tree_file_id(trans_id)
496
if self._tree.inventory[file_id].kind in ('directory',
498
parents.append(trans_id)
500
for parent_id in parents:
501
# ensure that all children are registered with the transaction
502
list(self.iter_tree_children(parent_id))
504
def iter_tree_children(self, parent_id):
505
"""Iterate through the entry's tree children, if any"""
507
path = self._tree_id_paths[parent_id]
511
children = os.listdir(self._tree.abspath(path))
513
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
517
for child in children:
518
childpath = joinpath(path, child)
519
if self._tree.is_control_filename(childpath):
521
yield self.trans_id_tree_path(childpath)
523
def has_named_child(self, by_parent, parent_id, name):
525
children = by_parent[parent_id]
528
for child in children:
529
if self.final_name(child) == name:
532
path = self._tree_id_paths[parent_id]
535
childpath = joinpath(path, name)
536
child_id = self._tree_path_ids.get(childpath)
538
return lexists(self._tree.abspath(childpath))
540
if tt.final_parent(child_id) != parent_id:
542
if child_id in tt._removed_contents:
543
# XXX What about dangling file-ids?
548
def _parent_loops(self):
549
"""No entry should be its own ancestor"""
551
for trans_id in self._new_parent:
554
while parent_id is not ROOT_PARENT:
556
parent_id = self.final_parent(parent_id)
557
if parent_id == trans_id:
558
conflicts.append(('parent loop', trans_id))
559
if parent_id in seen:
563
def _unversioned_parents(self, by_parent):
564
"""If parent directories are versioned, children must be versioned."""
566
for parent_id, children in by_parent.iteritems():
567
if parent_id is ROOT_PARENT:
569
if self.final_file_id(parent_id) is not None:
571
for child_id in children:
572
if self.final_file_id(child_id) is not None:
573
conflicts.append(('unversioned parent', parent_id))
577
def _improper_versioning(self):
578
"""Cannot version a file with no contents, or a bad type.
580
However, existing entries with no contents are okay.
583
for trans_id in self._new_id.iterkeys():
585
kind = self.final_kind(trans_id)
587
conflicts.append(('versioning no contents', trans_id))
589
if not InventoryEntry.versionable_kind(kind):
590
conflicts.append(('versioning bad kind', trans_id, kind))
593
def _executability_conflicts(self):
594
"""Check for bad executability changes.
596
Only versioned files may have their executability set, because
597
1. only versioned entries can have executability under windows
598
2. only files can be executable. (The execute bit on a directory
599
does not indicate searchability)
602
for trans_id in self._new_executability:
603
if self.final_file_id(trans_id) is None:
604
conflicts.append(('unversioned executability', trans_id))
607
non_file = self.final_kind(trans_id) != "file"
611
conflicts.append(('non-file executability', trans_id))
614
def _overwrite_conflicts(self):
615
"""Check for overwrites (not permitted on Win32)"""
617
for trans_id in self._new_contents:
619
self.tree_kind(trans_id)
622
if trans_id not in self._removed_contents:
623
conflicts.append(('overwrite', trans_id,
624
self.final_name(trans_id)))
627
def _duplicate_entries(self, by_parent):
628
"""No directory may have two entries with the same name."""
630
for children in by_parent.itervalues():
631
name_ids = [(self.final_name(t), t) for t in children]
635
for name, trans_id in name_ids:
636
if name == last_name:
637
conflicts.append(('duplicate', last_trans_id, trans_id,
640
kind = self.final_kind(trans_id)
643
file_id = self.final_file_id(trans_id)
644
if kind is not None or file_id is not None:
646
last_trans_id = trans_id
649
def _duplicate_ids(self):
650
"""Each inventory id may only be used once"""
652
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
654
active_tree_ids = set((f for f in self._tree.inventory if
655
f not in removed_tree_ids))
656
for trans_id, file_id in self._new_id.iteritems():
657
if file_id in active_tree_ids:
658
old_trans_id = self.trans_id_tree_file_id(file_id)
659
conflicts.append(('duplicate id', old_trans_id, trans_id))
662
def _parent_type_conflicts(self, by_parent):
663
"""parents must have directory 'contents'."""
665
for parent_id, children in by_parent.iteritems():
666
if parent_id is ROOT_PARENT:
668
if not self._any_contents(children):
670
for child in children:
672
self.final_kind(child)
676
kind = self.final_kind(parent_id)
680
conflicts.append(('missing parent', parent_id))
681
elif kind != "directory":
682
conflicts.append(('non-directory parent', parent_id))
685
def _any_contents(self, trans_ids):
686
"""Return true if any of the trans_ids, will have contents."""
687
for trans_id in trans_ids:
689
kind = self.final_kind(trans_id)
696
"""Apply all changes to the inventory and filesystem.
698
If filesystem or inventory conflicts are present, MalformedTransform
701
conflicts = self.find_conflicts()
702
if len(conflicts) != 0:
703
raise MalformedTransform(conflicts=conflicts)
705
inv = self._tree.inventory
706
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
708
child_pb.update('Apply phase', 0, 2)
709
self._apply_removals(inv, limbo_inv)
710
child_pb.update('Apply phase', 1, 2)
711
modified_paths = self._apply_insertions(inv, limbo_inv)
714
self._tree._write_inventory(inv)
717
return _TransformResults(modified_paths)
719
def _limbo_name(self, trans_id):
720
"""Generate the limbo name of a file"""
721
return pathjoin(self._limbodir, trans_id)
723
def _apply_removals(self, inv, limbo_inv):
724
"""Perform tree operations that remove directory/inventory names.
726
That is, delete files that are to be deleted, and put any files that
727
need renaming into limbo. This must be done in strict child-to-parent
730
tree_paths = list(self._tree_path_ids.iteritems())
731
tree_paths.sort(reverse=True)
732
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
734
for num, data in enumerate(tree_paths):
735
path, trans_id = data
736
child_pb.update('removing file', num, len(tree_paths))
737
full_path = self._tree.abspath(path)
738
if trans_id in self._removed_contents:
739
delete_any(full_path)
740
elif trans_id in self._new_name or trans_id in \
743
os.rename(full_path, self._limbo_name(trans_id))
745
if e.errno != errno.ENOENT:
747
if trans_id in self._removed_id:
748
if trans_id == self._new_root:
749
file_id = self._tree.inventory.root.file_id
751
file_id = self.tree_file_id(trans_id)
753
elif trans_id in self._new_name or trans_id in self._new_parent:
754
file_id = self.tree_file_id(trans_id)
755
if file_id is not None:
756
limbo_inv[trans_id] = inv[file_id]
761
def _apply_insertions(self, inv, limbo_inv):
762
"""Perform tree operations that insert directory/inventory names.
764
That is, create any files that need to be created, and restore from
765
limbo any files that needed renaming. This must be done in strict
766
parent-to-child order.
768
new_paths = self.new_paths()
770
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
772
for num, (path, trans_id) in enumerate(new_paths):
773
child_pb.update('adding file', num, len(new_paths))
775
kind = self._new_contents[trans_id]
777
kind = contents = None
778
if trans_id in self._new_contents or \
779
self.path_changed(trans_id):
780
full_path = self._tree.abspath(path)
782
os.rename(self._limbo_name(trans_id), full_path)
784
# We may be renaming a dangling inventory id
785
if e.errno != errno.ENOENT:
787
if trans_id in self._new_contents:
788
modified_paths.append(full_path)
789
del self._new_contents[trans_id]
791
if trans_id in self._new_id:
793
kind = file_kind(self._tree.abspath(path))
794
inv.add_path(path, kind, self._new_id[trans_id])
795
elif trans_id in self._new_name or trans_id in\
797
entry = limbo_inv.get(trans_id)
798
if entry is not None:
799
entry.name = self.final_name(trans_id)
800
parent_path = os.path.dirname(path)
802
self._tree.inventory.path2id(parent_path)
805
# requires files and inventory entries to be in place
806
if trans_id in self._new_executability:
807
self._set_executability(path, inv, trans_id)
810
return modified_paths
812
def _set_executability(self, path, inv, trans_id):
813
"""Set the executability of versioned files """
814
file_id = inv.path2id(path)
815
new_executability = self._new_executability[trans_id]
816
inv[file_id].executable = new_executability
817
if supports_executable():
818
abspath = self._tree.abspath(path)
819
current_mode = os.stat(abspath).st_mode
820
if new_executability:
823
to_mode = current_mode | (0100 & ~umask)
824
# Enable x-bit for others only if they can read it.
825
if current_mode & 0004:
826
to_mode |= 0001 & ~umask
827
if current_mode & 0040:
828
to_mode |= 0010 & ~umask
830
to_mode = current_mode & ~0111
831
os.chmod(abspath, to_mode)
833
def _new_entry(self, name, parent_id, file_id):
834
"""Helper function to create a new filesystem entry."""
835
trans_id = self.create_path(name, parent_id)
836
if file_id is not None:
837
self.version_file(file_id, trans_id)
840
def new_file(self, name, parent_id, contents, file_id=None,
842
"""Convenience method to create files.
844
name is the name of the file to create.
845
parent_id is the transaction id of the parent directory of the file.
846
contents is an iterator of bytestrings, which will be used to produce
848
:param file_id: The inventory ID of the file, if it is to be versioned.
849
:param executable: Only valid when a file_id has been supplied.
851
trans_id = self._new_entry(name, parent_id, file_id)
852
# TODO: rather than scheduling a set_executable call,
853
# have create_file create the file with the right mode.
854
self.create_file(contents, trans_id)
855
if executable is not None:
856
self.set_executability(executable, trans_id)
859
def new_directory(self, name, parent_id, file_id=None):
860
"""Convenience method to create directories.
862
name is the name of the directory to create.
863
parent_id is the transaction id of the parent directory of the
865
file_id is the inventory ID of the directory, if it is to be versioned.
867
trans_id = self._new_entry(name, parent_id, file_id)
868
self.create_directory(trans_id)
871
def new_symlink(self, name, parent_id, target, file_id=None):
872
"""Convenience method to create symbolic link.
874
name is the name of the symlink to create.
875
parent_id is the transaction id of the parent directory of the symlink.
876
target is a bytestring of the target of the symlink.
877
file_id is the inventory ID of the file, if it is to be versioned.
879
trans_id = self._new_entry(name, parent_id, file_id)
880
self.create_symlink(target, trans_id)
883
def joinpath(parent, child):
884
"""Join tree-relative paths, handling the tree root specially"""
885
if parent is None or parent == "":
888
return pathjoin(parent, child)
891
class FinalPaths(object):
892
"""Make path calculation cheap by memoizing paths.
894
The underlying tree must not be manipulated between calls, or else
895
the results will likely be incorrect.
897
def __init__(self, transform):
898
object.__init__(self)
899
self._known_paths = {}
900
self.transform = transform
902
def _determine_path(self, trans_id):
903
if trans_id == self.transform.root:
905
name = self.transform.final_name(trans_id)
906
parent_id = self.transform.final_parent(trans_id)
907
if parent_id == self.transform.root:
910
return pathjoin(self.get_path(parent_id), name)
912
def get_path(self, trans_id):
913
"""Find the final path associated with a trans_id"""
914
if trans_id not in self._known_paths:
915
self._known_paths[trans_id] = self._determine_path(trans_id)
916
return self._known_paths[trans_id]
918
def topology_sorted_ids(tree):
919
"""Determine the topological order of the ids in a tree"""
920
file_ids = list(tree)
921
file_ids.sort(key=tree.id2path)
924
def build_tree(tree, wt):
925
"""Create working tree for a branch, using a Transaction."""
927
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
928
pp = ProgressPhase("Build phase", 2, top_pb)
929
tt = TreeTransform(wt)
932
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
933
file_ids = topology_sorted_ids(tree)
934
pb = bzrlib.ui.ui_factory.nested_progress_bar()
936
for num, file_id in enumerate(file_ids):
937
pb.update("Building tree", num, len(file_ids))
938
entry = tree.inventory[file_id]
939
if entry.parent_id is None:
941
if entry.parent_id not in file_trans_id:
942
raise repr(entry.parent_id)
943
parent_id = file_trans_id[entry.parent_id]
944
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
954
def new_by_entry(tt, entry, parent_id, tree):
955
"""Create a new file according to its inventory entry"""
959
contents = tree.get_file(entry.file_id).readlines()
960
executable = tree.is_executable(entry.file_id)
961
return tt.new_file(name, parent_id, contents, entry.file_id,
963
elif kind == 'directory':
964
return tt.new_directory(name, parent_id, entry.file_id)
965
elif kind == 'symlink':
966
target = tree.get_symlink_target(entry.file_id)
967
return tt.new_symlink(name, parent_id, target, entry.file_id)
969
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
970
"""Create new file contents according to an inventory entry."""
971
if entry.kind == "file":
973
lines = tree.get_file(entry.file_id).readlines()
974
tt.create_file(lines, trans_id, mode_id=mode_id)
975
elif entry.kind == "symlink":
976
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
977
elif entry.kind == "directory":
978
tt.create_directory(trans_id)
980
def create_entry_executability(tt, entry, trans_id):
981
"""Set the executability of a trans_id according to an inventory entry"""
982
if entry.kind == "file":
983
tt.set_executability(entry.executable, trans_id)
986
def find_interesting(working_tree, target_tree, filenames):
987
"""Find the ids corresponding to specified filenames."""
989
interesting_ids = None
991
interesting_ids = set()
992
for tree_path in filenames:
994
for tree in (working_tree, target_tree):
995
file_id = tree.inventory.path2id(tree_path)
996
if file_id is not None:
997
interesting_ids.add(file_id)
1000
raise NotVersionedError(path=tree_path)
1001
return interesting_ids
1004
def change_entry(tt, file_id, working_tree, target_tree,
1005
trans_id_file_id, backups, trans_id, by_parent):
1006
"""Replace a file_id's contents with those from a target tree."""
1007
e_trans_id = trans_id_file_id(file_id)
1008
entry = target_tree.inventory[file_id]
1009
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1012
mode_id = e_trans_id
1015
tt.delete_contents(e_trans_id)
1017
parent_trans_id = trans_id_file_id(entry.parent_id)
1018
backup_name = get_backup_name(entry, by_parent,
1019
parent_trans_id, tt)
1020
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1021
tt.unversion_file(e_trans_id)
1022
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1023
tt.version_file(file_id, e_trans_id)
1024
trans_id[file_id] = e_trans_id
1025
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1026
create_entry_executability(tt, entry, e_trans_id)
1029
tt.set_executability(entry.executable, e_trans_id)
1030
if tt.final_name(e_trans_id) != entry.name:
1033
parent_id = tt.final_parent(e_trans_id)
1034
parent_file_id = tt.final_file_id(parent_id)
1035
if parent_file_id != entry.parent_id:
1040
parent_trans_id = trans_id_file_id(entry.parent_id)
1041
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1044
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1045
"""Produce a backup-style name that appears to be available"""
1049
yield "%s.~%d~" % (entry.name, counter)
1051
for name in name_gen():
1052
if not tt.has_named_child(by_parent, parent_trans_id, name):
1055
def _entry_changes(file_id, entry, working_tree):
1056
"""Determine in which ways the inventory entry has changed.
1058
Returns booleans: has_contents, content_mod, meta_mod
1059
has_contents means there are currently contents, but they differ
1060
contents_mod means contents need to be modified
1061
meta_mod means the metadata needs to be modified
1063
cur_entry = working_tree.inventory[file_id]
1065
working_kind = working_tree.kind(file_id)
1068
if e.errno != errno.ENOENT:
1070
has_contents = False
1073
if has_contents is True:
1074
real_e_kind = entry.kind
1075
if real_e_kind == 'root_directory':
1076
real_e_kind = 'directory'
1077
if real_e_kind != working_kind:
1078
contents_mod, meta_mod = True, False
1080
cur_entry._read_tree_state(working_tree.id2path(file_id),
1082
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1083
cur_entry._forget_tree_state()
1084
return has_contents, contents_mod, meta_mod
1087
def revert(working_tree, target_tree, filenames, backups=False,
1088
pb=DummyProgress()):
1089
"""Revert a working tree's contents to those of a target tree."""
1090
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1091
def interesting(file_id):
1092
return interesting_ids is None or file_id in interesting_ids
1094
tt = TreeTransform(working_tree, pb)
1096
merge_modified = working_tree.merge_modified()
1098
def trans_id_file_id(file_id):
1100
return trans_id[file_id]
1102
return tt.trans_id_tree_file_id(file_id)
1104
pp = ProgressPhase("Revert phase", 4, pb)
1106
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1108
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1110
by_parent = tt.by_parent()
1111
for id_num, file_id in enumerate(sorted_interesting):
1112
child_pb.update("Reverting file", id_num+1,
1113
len(sorted_interesting))
1114
if file_id not in working_tree.inventory:
1115
entry = target_tree.inventory[file_id]
1116
parent_id = trans_id_file_id(entry.parent_id)
1117
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1118
trans_id[file_id] = e_trans_id
1120
backup_this = backups
1121
if file_id in merge_modified:
1123
del merge_modified[file_id]
1124
change_entry(tt, file_id, working_tree, target_tree,
1125
trans_id_file_id, backup_this, trans_id,
1130
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1131
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1133
for id_num, file_id in enumerate(wt_interesting):
1134
child_pb.update("New file check", id_num+1,
1135
len(sorted_interesting))
1136
if file_id not in target_tree:
1137
trans_id = tt.trans_id_tree_file_id(file_id)
1138
tt.unversion_file(trans_id)
1139
if file_id in merge_modified:
1140
tt.delete_contents(trans_id)
1141
del merge_modified[file_id]
1145
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1147
raw_conflicts = resolve_conflicts(tt, child_pb)
1150
conflicts = cook_conflicts(raw_conflicts, tt)
1151
for conflict in conflicts:
1155
working_tree.set_merge_modified({})
1162
def resolve_conflicts(tt, pb=DummyProgress()):
1163
"""Make many conflict-resolution attempts, but die if they fail"""
1164
new_conflicts = set()
1167
pb.update('Resolution pass', n+1, 10)
1168
conflicts = tt.find_conflicts()
1169
if len(conflicts) == 0:
1170
return new_conflicts
1171
new_conflicts.update(conflict_pass(tt, conflicts))
1172
raise MalformedTransform(conflicts=conflicts)
1177
def conflict_pass(tt, conflicts):
1178
"""Resolve some classes of conflicts."""
1179
new_conflicts = set()
1180
for c_type, conflict in ((c[0], c) for c in conflicts):
1181
if c_type == 'duplicate id':
1182
tt.unversion_file(conflict[1])
1183
new_conflicts.add((c_type, 'Unversioned existing file',
1184
conflict[1], conflict[2], ))
1185
elif c_type == 'duplicate':
1186
# files that were renamed take precedence
1187
new_name = tt.final_name(conflict[1])+'.moved'
1188
final_parent = tt.final_parent(conflict[1])
1189
if tt.path_changed(conflict[1]):
1190
tt.adjust_path(new_name, final_parent, conflict[2])
1191
new_conflicts.add((c_type, 'Moved existing file to',
1192
conflict[2], conflict[1]))
1194
tt.adjust_path(new_name, final_parent, conflict[1])
1195
new_conflicts.add((c_type, 'Moved existing file to',
1196
conflict[1], conflict[2]))
1197
elif c_type == 'parent loop':
1198
# break the loop by undoing one of the ops that caused the loop
1200
while not tt.path_changed(cur):
1201
cur = tt.final_parent(cur)
1202
new_conflicts.add((c_type, 'Cancelled move', cur,
1203
tt.final_parent(cur),))
1204
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1206
elif c_type == 'missing parent':
1207
trans_id = conflict[1]
1209
tt.cancel_deletion(trans_id)
1210
new_conflicts.add((c_type, 'Not deleting', trans_id))
1212
tt.create_directory(trans_id)
1213
new_conflicts.add((c_type, 'Created directory.', trans_id))
1214
elif c_type == 'unversioned parent':
1215
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1216
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1217
return new_conflicts
1220
def cook_conflicts(raw_conflicts, tt):
1221
"""Generate a list of cooked conflicts, sorted by file path"""
1222
from bzrlib.conflicts import Conflict
1223
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1224
return sorted(conflict_iter, key=Conflict.sort_key)
1227
def iter_cook_conflicts(raw_conflicts, tt):
1228
from bzrlib.conflicts import Conflict
1230
for conflict in raw_conflicts:
1231
c_type = conflict[0]
1232
action = conflict[1]
1233
modified_path = fp.get_path(conflict[2])
1234
modified_id = tt.final_file_id(conflict[2])
1235
if len(conflict) == 3:
1236
yield Conflict.factory(c_type, action=action, path=modified_path,
1237
file_id=modified_id)
1240
conflicting_path = fp.get_path(conflict[3])
1241
conflicting_id = tt.final_file_id(conflict[3])
1242
yield Conflict.factory(c_type, action=action, path=modified_path,
1243
file_id=modified_id,
1244
conflict_path=conflicting_path,
1245
conflict_file_id=conflicting_id)