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,
26
delete_any, local_path_from_url)
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 = local_path_from_url(
83
control_files.controlfilename('limbo'))
85
os.mkdir(self._limbodir)
87
if e.errno == errno.EEXIST:
88
raise ExistingLimbo(self._limbodir)
96
self._new_contents = {}
97
self._removed_contents = set()
98
self._new_executability = {}
100
self._non_present_ids = {}
102
self._removed_id = set()
103
self._tree_path_ids = {}
104
self._tree_id_paths = {}
105
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
109
def __get_root(self):
110
return self._new_root
112
root = property(__get_root)
115
"""Release the working tree lock, if held, clean up limbo dir."""
116
if self._tree is None:
119
for trans_id, kind in self._new_contents.iteritems():
120
path = self._limbo_name(trans_id)
121
if kind == "directory":
126
os.rmdir(self._limbodir)
128
# We don't especially care *why* the dir is immortal.
129
raise ImmortalLimbo(self._limbodir)
134
def _assign_id(self):
135
"""Produce a new tranform id"""
136
new_id = "new-%s" % self._id_number
140
def create_path(self, name, parent):
141
"""Assign a transaction id to a new path"""
142
trans_id = self._assign_id()
143
unique_add(self._new_name, trans_id, name)
144
unique_add(self._new_parent, trans_id, parent)
147
def adjust_path(self, name, parent, trans_id):
148
"""Change the path that is assigned to a transaction id."""
149
if trans_id == self._new_root:
151
self._new_name[trans_id] = name
152
self._new_parent[trans_id] = parent
154
def adjust_root_path(self, name, parent):
155
"""Emulate moving the root by moving all children, instead.
157
We do this by undoing the association of root's transaction id with the
158
current tree. This allows us to create a new directory with that
159
transaction id. We unversion the root directory and version the
160
physically new directory, and hope someone versions the tree root
163
old_root = self._new_root
164
old_root_file_id = self.final_file_id(old_root)
165
# force moving all children of root
166
for child_id in self.iter_tree_children(old_root):
167
if child_id != parent:
168
self.adjust_path(self.final_name(child_id),
169
self.final_parent(child_id), child_id)
170
file_id = self.final_file_id(child_id)
171
if file_id is not None:
172
self.unversion_file(child_id)
173
self.version_file(file_id, child_id)
175
# the physical root needs a new transaction id
176
self._tree_path_ids.pop("")
177
self._tree_id_paths.pop(old_root)
178
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
179
if parent == old_root:
180
parent = self._new_root
181
self.adjust_path(name, parent, old_root)
182
self.create_directory(old_root)
183
self.version_file(old_root_file_id, old_root)
184
self.unversion_file(self._new_root)
186
def trans_id_tree_file_id(self, inventory_id):
187
"""Determine the transaction id of a working tree file.
189
This reflects only files that already exist, not ones that will be
190
added by transactions.
192
path = self._tree.inventory.id2path(inventory_id)
193
return self.trans_id_tree_path(path)
195
def trans_id_file_id(self, file_id):
196
"""Determine or set the transaction id associated with a file ID.
197
A new id is only created for file_ids that were never present. If
198
a transaction has been unversioned, it is deliberately still returned.
199
(this will likely lead to an unversioned parent conflict.)
201
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
202
return self._r_new_id[file_id]
203
elif file_id in self._tree.inventory:
204
return self.trans_id_tree_file_id(file_id)
205
elif file_id in self._non_present_ids:
206
return self._non_present_ids[file_id]
208
trans_id = self._assign_id()
209
self._non_present_ids[file_id] = trans_id
212
def canonical_path(self, path):
213
"""Get the canonical tree-relative path"""
214
# don't follow final symlinks
215
dirname, basename = os.path.split(self._tree.abspath(path))
216
dirname = os.path.realpath(dirname)
217
return self._tree.relpath(pathjoin(dirname, basename))
219
def trans_id_tree_path(self, path):
220
"""Determine (and maybe set) the transaction ID for a tree path."""
221
path = self.canonical_path(path)
222
if path not in self._tree_path_ids:
223
self._tree_path_ids[path] = self._assign_id()
224
self._tree_id_paths[self._tree_path_ids[path]] = path
225
return self._tree_path_ids[path]
227
def get_tree_parent(self, trans_id):
228
"""Determine id of the parent in the tree."""
229
path = self._tree_id_paths[trans_id]
232
return self.trans_id_tree_path(os.path.dirname(path))
234
def create_file(self, contents, trans_id, mode_id=None):
235
"""Schedule creation of a new file.
239
Contents is an iterator of strings, all of which will be written
240
to the target destination.
242
New file takes the permissions of any existing file with that id,
243
unless mode_id is specified.
245
f = file(self._limbo_name(trans_id), 'wb')
246
unique_add(self._new_contents, trans_id, 'file')
247
for segment in contents:
250
self._set_mode(trans_id, mode_id, S_ISREG)
252
def _set_mode(self, trans_id, mode_id, typefunc):
253
"""Set the mode of new file contents.
254
The mode_id is the existing file to get the mode from (often the same
255
as trans_id). The operation is only performed if there's a mode match
256
according to typefunc.
261
old_path = self._tree_id_paths[mode_id]
265
mode = os.stat(old_path).st_mode
267
if e.errno == errno.ENOENT:
272
os.chmod(self._limbo_name(trans_id), mode)
274
def create_directory(self, trans_id):
275
"""Schedule creation of a new directory.
277
See also new_directory.
279
os.mkdir(self._limbo_name(trans_id))
280
unique_add(self._new_contents, trans_id, 'directory')
282
def create_symlink(self, target, trans_id):
283
"""Schedule creation of a new symbolic link.
285
target is a bytestring.
286
See also new_symlink.
288
os.symlink(target, self._limbo_name(trans_id))
289
unique_add(self._new_contents, trans_id, 'symlink')
291
def cancel_creation(self, trans_id):
292
"""Cancel the creation of new file contents."""
293
del self._new_contents[trans_id]
294
delete_any(self._limbo_name(trans_id))
296
def delete_contents(self, trans_id):
297
"""Schedule the contents of a path entry for deletion"""
298
self.tree_kind(trans_id)
299
self._removed_contents.add(trans_id)
301
def cancel_deletion(self, trans_id):
302
"""Cancel a scheduled deletion"""
303
self._removed_contents.remove(trans_id)
305
def unversion_file(self, trans_id):
306
"""Schedule a path entry to become unversioned"""
307
self._removed_id.add(trans_id)
309
def delete_versioned(self, trans_id):
310
"""Delete and unversion a versioned file"""
311
self.delete_contents(trans_id)
312
self.unversion_file(trans_id)
314
def set_executability(self, executability, trans_id):
315
"""Schedule setting of the 'execute' bit
316
To unschedule, set to None
318
if executability is None:
319
del self._new_executability[trans_id]
321
unique_add(self._new_executability, trans_id, executability)
323
def version_file(self, file_id, trans_id):
324
"""Schedule a file to become versioned."""
325
assert file_id is not None
326
unique_add(self._new_id, trans_id, file_id)
327
unique_add(self._r_new_id, file_id, trans_id)
329
def cancel_versioning(self, trans_id):
330
"""Undo a previous versioning of a file"""
331
file_id = self._new_id[trans_id]
332
del self._new_id[trans_id]
333
del self._r_new_id[file_id]
336
"""Determine the paths of all new and changed files"""
338
fp = FinalPaths(self)
339
for id_set in (self._new_name, self._new_parent, self._new_contents,
340
self._new_id, self._new_executability):
341
new_ids.update(id_set)
342
new_paths = [(fp.get_path(t), t) for t in new_ids]
346
def tree_kind(self, trans_id):
347
"""Determine the file kind in the working tree.
349
Raises NoSuchFile if the file does not exist
351
path = self._tree_id_paths.get(trans_id)
353
raise NoSuchFile(None)
355
return file_kind(self._tree.abspath(path))
357
if e.errno != errno.ENOENT:
360
raise NoSuchFile(path)
362
def final_kind(self, trans_id):
363
"""Determine the final file kind, after any changes applied.
365
Raises NoSuchFile if the file does not exist/has no contents.
366
(It is conceivable that a path would be created without the
367
corresponding contents insertion command)
369
if trans_id in self._new_contents:
370
return self._new_contents[trans_id]
371
elif trans_id in self._removed_contents:
372
raise NoSuchFile(None)
374
return self.tree_kind(trans_id)
376
def tree_file_id(self, trans_id):
377
"""Determine the file id associated with the trans_id in the tree"""
379
path = self._tree_id_paths[trans_id]
381
# the file is a new, unversioned file, or invalid trans_id
383
# the file is old; the old id is still valid
384
if self._new_root == trans_id:
385
return self._tree.inventory.root.file_id
386
return self._tree.inventory.path2id(path)
388
def final_file_id(self, trans_id):
389
"""Determine the file id after any changes are applied, or None.
391
None indicates that the file will not be versioned after changes are
395
# there is a new id for this file
396
assert self._new_id[trans_id] is not None
397
return self._new_id[trans_id]
399
if trans_id in self._removed_id:
401
return self.tree_file_id(trans_id)
403
def inactive_file_id(self, trans_id):
404
"""Return the inactive file_id associated with a transaction id.
405
That is, the one in the tree or in non_present_ids.
406
The file_id may actually be active, too.
408
file_id = self.tree_file_id(trans_id)
409
if file_id is not None:
411
for key, value in self._non_present_ids.iteritems():
412
if value == trans_id:
415
def final_parent(self, trans_id):
416
"""Determine the parent file_id, after any changes are applied.
418
ROOT_PARENT is returned for the tree root.
421
return self._new_parent[trans_id]
423
return self.get_tree_parent(trans_id)
425
def final_name(self, trans_id):
426
"""Determine the final filename, after all changes are applied."""
428
return self._new_name[trans_id]
430
return os.path.basename(self._tree_id_paths[trans_id])
433
"""Return a map of parent: children for known parents.
435
Only new paths and parents of tree files with assigned ids are used.
438
items = list(self._new_parent.iteritems())
439
items.extend((t, self.final_parent(t)) for t in
440
self._tree_id_paths.keys())
441
for trans_id, parent_id in items:
442
if parent_id not in by_parent:
443
by_parent[parent_id] = set()
444
by_parent[parent_id].add(trans_id)
447
def path_changed(self, trans_id):
448
"""Return True if a trans_id's path has changed."""
449
return trans_id in self._new_name or trans_id in self._new_parent
451
def find_conflicts(self):
452
"""Find any violations of inventory or filesystem invariants"""
453
if self.__done is True:
454
raise ReusingTransform()
456
# ensure all children of all existent parents are known
457
# all children of non-existent parents are known, by definition.
458
self._add_tree_children()
459
by_parent = self.by_parent()
460
conflicts.extend(self._unversioned_parents(by_parent))
461
conflicts.extend(self._parent_loops())
462
conflicts.extend(self._duplicate_entries(by_parent))
463
conflicts.extend(self._duplicate_ids())
464
conflicts.extend(self._parent_type_conflicts(by_parent))
465
conflicts.extend(self._improper_versioning())
466
conflicts.extend(self._executability_conflicts())
467
conflicts.extend(self._overwrite_conflicts())
470
def _add_tree_children(self):
471
"""Add all the children of all active parents to the known paths.
473
Active parents are those which gain children, and those which are
474
removed. This is a necessary first step in detecting conflicts.
476
parents = self.by_parent().keys()
477
parents.extend([t for t in self._removed_contents if
478
self.tree_kind(t) == 'directory'])
479
for trans_id in self._removed_id:
480
file_id = self.tree_file_id(trans_id)
481
if self._tree.inventory[file_id].kind in ('directory',
483
parents.append(trans_id)
485
for parent_id in parents:
486
# ensure that all children are registered with the transaction
487
list(self.iter_tree_children(parent_id))
489
def iter_tree_children(self, parent_id):
490
"""Iterate through the entry's tree children, if any"""
492
path = self._tree_id_paths[parent_id]
496
children = os.listdir(self._tree.abspath(path))
498
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
502
for child in children:
503
childpath = joinpath(path, child)
504
if self._tree.is_control_filename(childpath):
506
yield self.trans_id_tree_path(childpath)
508
def has_named_child(self, by_parent, parent_id, name):
510
children = by_parent[parent_id]
513
for child in children:
514
if self.final_name(child) == name:
517
path = self._tree_id_paths[parent_id]
520
childpath = joinpath(path, name)
521
child_id = self._tree_path_ids.get(childpath)
523
return lexists(self._tree.abspath(childpath))
525
if tt.final_parent(child_id) != parent_id:
527
if child_id in tt._removed_contents:
528
# XXX What about dangling file-ids?
533
def _parent_loops(self):
534
"""No entry should be its own ancestor"""
536
for trans_id in self._new_parent:
539
while parent_id is not ROOT_PARENT:
541
parent_id = self.final_parent(parent_id)
542
if parent_id == trans_id:
543
conflicts.append(('parent loop', trans_id))
544
if parent_id in seen:
548
def _unversioned_parents(self, by_parent):
549
"""If parent directories are versioned, children must be versioned."""
551
for parent_id, children in by_parent.iteritems():
552
if parent_id is ROOT_PARENT:
554
if self.final_file_id(parent_id) is not None:
556
for child_id in children:
557
if self.final_file_id(child_id) is not None:
558
conflicts.append(('unversioned parent', parent_id))
562
def _improper_versioning(self):
563
"""Cannot version a file with no contents, or a bad type.
565
However, existing entries with no contents are okay.
568
for trans_id in self._new_id.iterkeys():
570
kind = self.final_kind(trans_id)
572
conflicts.append(('versioning no contents', trans_id))
574
if not InventoryEntry.versionable_kind(kind):
575
conflicts.append(('versioning bad kind', trans_id, kind))
578
def _executability_conflicts(self):
579
"""Check for bad executability changes.
581
Only versioned files may have their executability set, because
582
1. only versioned entries can have executability under windows
583
2. only files can be executable. (The execute bit on a directory
584
does not indicate searchability)
587
for trans_id in self._new_executability:
588
if self.final_file_id(trans_id) is None:
589
conflicts.append(('unversioned executability', trans_id))
592
non_file = self.final_kind(trans_id) != "file"
596
conflicts.append(('non-file executability', trans_id))
599
def _overwrite_conflicts(self):
600
"""Check for overwrites (not permitted on Win32)"""
602
for trans_id in self._new_contents:
604
self.tree_kind(trans_id)
607
if trans_id not in self._removed_contents:
608
conflicts.append(('overwrite', trans_id,
609
self.final_name(trans_id)))
612
def _duplicate_entries(self, by_parent):
613
"""No directory may have two entries with the same name."""
615
for children in by_parent.itervalues():
616
name_ids = [(self.final_name(t), t) for t in children]
620
for name, trans_id in name_ids:
621
if name == last_name:
622
conflicts.append(('duplicate', last_trans_id, trans_id,
625
kind = self.final_kind(trans_id)
628
file_id = self.final_file_id(trans_id)
629
if kind is not None or file_id is not None:
631
last_trans_id = trans_id
634
def _duplicate_ids(self):
635
"""Each inventory id may only be used once"""
637
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
639
active_tree_ids = set((f for f in self._tree.inventory if
640
f not in removed_tree_ids))
641
for trans_id, file_id in self._new_id.iteritems():
642
if file_id in active_tree_ids:
643
old_trans_id = self.trans_id_tree_file_id(file_id)
644
conflicts.append(('duplicate id', old_trans_id, trans_id))
647
def _parent_type_conflicts(self, by_parent):
648
"""parents must have directory 'contents'."""
650
for parent_id, children in by_parent.iteritems():
651
if parent_id is ROOT_PARENT:
653
if not self._any_contents(children):
655
for child in children:
657
self.final_kind(child)
661
kind = self.final_kind(parent_id)
665
conflicts.append(('missing parent', parent_id))
666
elif kind != "directory":
667
conflicts.append(('non-directory parent', parent_id))
670
def _any_contents(self, trans_ids):
671
"""Return true if any of the trans_ids, will have contents."""
672
for trans_id in trans_ids:
674
kind = self.final_kind(trans_id)
681
"""Apply all changes to the inventory and filesystem.
683
If filesystem or inventory conflicts are present, MalformedTransform
686
conflicts = self.find_conflicts()
687
if len(conflicts) != 0:
688
raise MalformedTransform(conflicts=conflicts)
690
inv = self._tree.inventory
691
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
693
child_pb.update('Apply phase', 0, 2)
694
self._apply_removals(inv, limbo_inv)
695
child_pb.update('Apply phase', 1, 2)
696
modified_paths = self._apply_insertions(inv, limbo_inv)
699
self._tree._write_inventory(inv)
702
return _TransformResults(modified_paths)
704
def _limbo_name(self, trans_id):
705
"""Generate the limbo name of a file"""
706
return pathjoin(self._limbodir, trans_id)
708
def _apply_removals(self, inv, limbo_inv):
709
"""Perform tree operations that remove directory/inventory names.
711
That is, delete files that are to be deleted, and put any files that
712
need renaming into limbo. This must be done in strict child-to-parent
715
tree_paths = list(self._tree_path_ids.iteritems())
716
tree_paths.sort(reverse=True)
717
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
719
for num, data in enumerate(tree_paths):
720
path, trans_id = data
721
child_pb.update('removing file', num, len(tree_paths))
722
full_path = self._tree.abspath(path)
723
if trans_id in self._removed_contents:
724
delete_any(full_path)
725
elif trans_id in self._new_name or trans_id in \
728
os.rename(full_path, self._limbo_name(trans_id))
730
if e.errno != errno.ENOENT:
732
if trans_id in self._removed_id:
733
if trans_id == self._new_root:
734
file_id = self._tree.inventory.root.file_id
736
file_id = self.tree_file_id(trans_id)
738
elif trans_id in self._new_name or trans_id in self._new_parent:
739
file_id = self.tree_file_id(trans_id)
740
if file_id is not None:
741
limbo_inv[trans_id] = inv[file_id]
746
def _apply_insertions(self, inv, limbo_inv):
747
"""Perform tree operations that insert directory/inventory names.
749
That is, create any files that need to be created, and restore from
750
limbo any files that needed renaming. This must be done in strict
751
parent-to-child order.
753
new_paths = self.new_paths()
755
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
757
for num, (path, trans_id) in enumerate(new_paths):
758
child_pb.update('adding file', num, len(new_paths))
760
kind = self._new_contents[trans_id]
762
kind = contents = None
763
if trans_id in self._new_contents or \
764
self.path_changed(trans_id):
765
full_path = self._tree.abspath(path)
767
os.rename(self._limbo_name(trans_id), full_path)
769
# We may be renaming a dangling inventory id
770
if e.errno != errno.ENOENT:
772
if trans_id in self._new_contents:
773
modified_paths.append(full_path)
774
del self._new_contents[trans_id]
776
if trans_id in self._new_id:
778
kind = file_kind(self._tree.abspath(path))
779
inv.add_path(path, kind, self._new_id[trans_id])
780
elif trans_id in self._new_name or trans_id in\
782
entry = limbo_inv.get(trans_id)
783
if entry is not None:
784
entry.name = self.final_name(trans_id)
785
parent_path = os.path.dirname(path)
787
self._tree.inventory.path2id(parent_path)
790
# requires files and inventory entries to be in place
791
if trans_id in self._new_executability:
792
self._set_executability(path, inv, trans_id)
795
return modified_paths
797
def _set_executability(self, path, inv, trans_id):
798
"""Set the executability of versioned files """
799
file_id = inv.path2id(path)
800
new_executability = self._new_executability[trans_id]
801
inv[file_id].executable = new_executability
802
if supports_executable():
803
abspath = self._tree.abspath(path)
804
current_mode = os.stat(abspath).st_mode
805
if new_executability:
808
to_mode = current_mode | (0100 & ~umask)
809
# Enable x-bit for others only if they can read it.
810
if current_mode & 0004:
811
to_mode |= 0001 & ~umask
812
if current_mode & 0040:
813
to_mode |= 0010 & ~umask
815
to_mode = current_mode & ~0111
816
os.chmod(abspath, to_mode)
818
def _new_entry(self, name, parent_id, file_id):
819
"""Helper function to create a new filesystem entry."""
820
trans_id = self.create_path(name, parent_id)
821
if file_id is not None:
822
self.version_file(file_id, trans_id)
825
def new_file(self, name, parent_id, contents, file_id=None,
827
"""Convenience method to create files.
829
name is the name of the file to create.
830
parent_id is the transaction id of the parent directory of the file.
831
contents is an iterator of bytestrings, which will be used to produce
833
file_id is the inventory ID of the file, if it is to be versioned.
835
trans_id = self._new_entry(name, parent_id, file_id)
836
self.create_file(contents, trans_id)
837
if executable is not None:
838
self.set_executability(executable, trans_id)
841
def new_directory(self, name, parent_id, file_id=None):
842
"""Convenience method to create directories.
844
name is the name of the directory to create.
845
parent_id is the transaction id of the parent directory of the
847
file_id is the inventory ID of the directory, if it is to be versioned.
849
trans_id = self._new_entry(name, parent_id, file_id)
850
self.create_directory(trans_id)
853
def new_symlink(self, name, parent_id, target, file_id=None):
854
"""Convenience method to create symbolic link.
856
name is the name of the symlink to create.
857
parent_id is the transaction id of the parent directory of the symlink.
858
target is a bytestring of the target of the symlink.
859
file_id is the inventory ID of the file, if it is to be versioned.
861
trans_id = self._new_entry(name, parent_id, file_id)
862
self.create_symlink(target, trans_id)
865
def joinpath(parent, child):
866
"""Join tree-relative paths, handling the tree root specially"""
867
if parent is None or parent == "":
870
return pathjoin(parent, child)
873
class FinalPaths(object):
874
"""Make path calculation cheap by memoizing paths.
876
The underlying tree must not be manipulated between calls, or else
877
the results will likely be incorrect.
879
def __init__(self, transform):
880
object.__init__(self)
881
self._known_paths = {}
882
self.transform = transform
884
def _determine_path(self, trans_id):
885
if trans_id == self.transform.root:
887
name = self.transform.final_name(trans_id)
888
parent_id = self.transform.final_parent(trans_id)
889
if parent_id == self.transform.root:
892
return pathjoin(self.get_path(parent_id), name)
894
def get_path(self, trans_id):
895
"""Find the final path associated with a trans_id"""
896
if trans_id not in self._known_paths:
897
self._known_paths[trans_id] = self._determine_path(trans_id)
898
return self._known_paths[trans_id]
900
def topology_sorted_ids(tree):
901
"""Determine the topological order of the ids in a tree"""
902
file_ids = list(tree)
903
file_ids.sort(key=tree.id2path)
906
def build_tree(tree, wt):
907
"""Create working tree for a branch, using a Transaction."""
909
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
910
pp = ProgressPhase("Build phase", 2, top_pb)
911
tt = TreeTransform(wt)
914
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
915
file_ids = topology_sorted_ids(tree)
916
pb = bzrlib.ui.ui_factory.nested_progress_bar()
918
for num, file_id in enumerate(file_ids):
919
pb.update("Building tree", num, len(file_ids))
920
entry = tree.inventory[file_id]
921
if entry.parent_id is None:
923
if entry.parent_id not in file_trans_id:
924
raise repr(entry.parent_id)
925
parent_id = file_trans_id[entry.parent_id]
926
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
936
def new_by_entry(tt, entry, parent_id, tree):
937
"""Create a new file according to its inventory entry"""
941
contents = tree.get_file(entry.file_id).readlines()
942
executable = tree.is_executable(entry.file_id)
943
return tt.new_file(name, parent_id, contents, entry.file_id,
945
elif kind == 'directory':
946
return tt.new_directory(name, parent_id, entry.file_id)
947
elif kind == 'symlink':
948
target = tree.get_symlink_target(entry.file_id)
949
return tt.new_symlink(name, parent_id, target, entry.file_id)
951
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
952
"""Create new file contents according to an inventory entry."""
953
if entry.kind == "file":
955
lines = tree.get_file(entry.file_id).readlines()
956
tt.create_file(lines, trans_id, mode_id=mode_id)
957
elif entry.kind == "symlink":
958
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
959
elif entry.kind == "directory":
960
tt.create_directory(trans_id)
962
def create_entry_executability(tt, entry, trans_id):
963
"""Set the executability of a trans_id according to an inventory entry"""
964
if entry.kind == "file":
965
tt.set_executability(entry.executable, trans_id)
968
def find_interesting(working_tree, target_tree, filenames):
969
"""Find the ids corresponding to specified filenames."""
971
interesting_ids = None
973
interesting_ids = set()
974
for tree_path in filenames:
976
for tree in (working_tree, target_tree):
977
file_id = tree.inventory.path2id(tree_path)
978
if file_id is not None:
979
interesting_ids.add(file_id)
982
raise NotVersionedError(path=tree_path)
983
return interesting_ids
986
def change_entry(tt, file_id, working_tree, target_tree,
987
trans_id_file_id, backups, trans_id, by_parent):
988
"""Replace a file_id's contents with those from a target tree."""
989
e_trans_id = trans_id_file_id(file_id)
990
entry = target_tree.inventory[file_id]
991
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
997
tt.delete_contents(e_trans_id)
999
parent_trans_id = trans_id_file_id(entry.parent_id)
1000
backup_name = get_backup_name(entry, by_parent,
1001
parent_trans_id, tt)
1002
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1003
tt.unversion_file(e_trans_id)
1004
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1005
tt.version_file(file_id, e_trans_id)
1006
trans_id[file_id] = e_trans_id
1007
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1008
create_entry_executability(tt, entry, e_trans_id)
1011
tt.set_executability(entry.executable, e_trans_id)
1012
if tt.final_name(e_trans_id) != entry.name:
1015
parent_id = tt.final_parent(e_trans_id)
1016
parent_file_id = tt.final_file_id(parent_id)
1017
if parent_file_id != entry.parent_id:
1022
parent_trans_id = trans_id_file_id(entry.parent_id)
1023
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1026
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1027
"""Produce a backup-style name that appears to be available"""
1031
yield "%s.~%d~" % (entry.name, counter)
1033
for name in name_gen():
1034
if not tt.has_named_child(by_parent, parent_trans_id, name):
1037
def _entry_changes(file_id, entry, working_tree):
1038
"""Determine in which ways the inventory entry has changed.
1040
Returns booleans: has_contents, content_mod, meta_mod
1041
has_contents means there are currently contents, but they differ
1042
contents_mod means contents need to be modified
1043
meta_mod means the metadata needs to be modified
1045
cur_entry = working_tree.inventory[file_id]
1047
working_kind = working_tree.kind(file_id)
1050
if e.errno != errno.ENOENT:
1052
has_contents = False
1055
if has_contents is True:
1056
real_e_kind = entry.kind
1057
if real_e_kind == 'root_directory':
1058
real_e_kind = 'directory'
1059
if real_e_kind != working_kind:
1060
contents_mod, meta_mod = True, False
1062
cur_entry._read_tree_state(working_tree.id2path(file_id),
1064
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1065
cur_entry._forget_tree_state()
1066
return has_contents, contents_mod, meta_mod
1069
def revert(working_tree, target_tree, filenames, backups=False,
1070
pb=DummyProgress()):
1071
"""Revert a working tree's contents to those of a target tree."""
1072
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1073
def interesting(file_id):
1074
return interesting_ids is None or file_id in interesting_ids
1076
tt = TreeTransform(working_tree, pb)
1078
merge_modified = working_tree.merge_modified()
1080
def trans_id_file_id(file_id):
1082
return trans_id[file_id]
1084
return tt.trans_id_tree_file_id(file_id)
1086
pp = ProgressPhase("Revert phase", 4, pb)
1088
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1090
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1092
by_parent = tt.by_parent()
1093
for id_num, file_id in enumerate(sorted_interesting):
1094
child_pb.update("Reverting file", id_num+1,
1095
len(sorted_interesting))
1096
if file_id not in working_tree.inventory:
1097
entry = target_tree.inventory[file_id]
1098
parent_id = trans_id_file_id(entry.parent_id)
1099
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1100
trans_id[file_id] = e_trans_id
1102
backup_this = backups
1103
if file_id in merge_modified:
1105
del merge_modified[file_id]
1106
change_entry(tt, file_id, working_tree, target_tree,
1107
trans_id_file_id, backup_this, trans_id,
1112
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1113
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1115
for id_num, file_id in enumerate(wt_interesting):
1116
child_pb.update("New file check", id_num+1,
1117
len(sorted_interesting))
1118
if file_id not in target_tree:
1119
trans_id = tt.trans_id_tree_file_id(file_id)
1120
tt.unversion_file(trans_id)
1121
if file_id in merge_modified:
1122
tt.delete_contents(trans_id)
1123
del merge_modified[file_id]
1127
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1129
raw_conflicts = resolve_conflicts(tt, child_pb)
1132
conflicts = cook_conflicts(raw_conflicts, tt)
1133
for conflict in conflicts:
1137
working_tree.set_merge_modified({})
1144
def resolve_conflicts(tt, pb=DummyProgress()):
1145
"""Make many conflict-resolution attempts, but die if they fail"""
1146
new_conflicts = set()
1149
pb.update('Resolution pass', n+1, 10)
1150
conflicts = tt.find_conflicts()
1151
if len(conflicts) == 0:
1152
return new_conflicts
1153
new_conflicts.update(conflict_pass(tt, conflicts))
1154
raise MalformedTransform(conflicts=conflicts)
1159
def conflict_pass(tt, conflicts):
1160
"""Resolve some classes of conflicts."""
1161
new_conflicts = set()
1162
for c_type, conflict in ((c[0], c) for c in conflicts):
1163
if c_type == 'duplicate id':
1164
tt.unversion_file(conflict[1])
1165
new_conflicts.add((c_type, 'Unversioned existing file',
1166
conflict[1], conflict[2], ))
1167
elif c_type == 'duplicate':
1168
# files that were renamed take precedence
1169
new_name = tt.final_name(conflict[1])+'.moved'
1170
final_parent = tt.final_parent(conflict[1])
1171
if tt.path_changed(conflict[1]):
1172
tt.adjust_path(new_name, final_parent, conflict[2])
1173
new_conflicts.add((c_type, 'Moved existing file to',
1174
conflict[2], conflict[1]))
1176
tt.adjust_path(new_name, final_parent, conflict[1])
1177
new_conflicts.add((c_type, 'Moved existing file to',
1178
conflict[1], conflict[2]))
1179
elif c_type == 'parent loop':
1180
# break the loop by undoing one of the ops that caused the loop
1182
while not tt.path_changed(cur):
1183
cur = tt.final_parent(cur)
1184
new_conflicts.add((c_type, 'Cancelled move', cur,
1185
tt.final_parent(cur),))
1186
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1188
elif c_type == 'missing parent':
1189
trans_id = conflict[1]
1191
tt.cancel_deletion(trans_id)
1192
new_conflicts.add((c_type, 'Not deleting', trans_id))
1194
tt.create_directory(trans_id)
1195
new_conflicts.add((c_type, 'Created directory.', trans_id))
1196
elif c_type == 'unversioned parent':
1197
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1198
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1199
return new_conflicts
1202
def cook_conflicts(raw_conflicts, tt):
1203
"""Generate a list of cooked conflicts, sorted by file path"""
1204
from bzrlib.conflicts import Conflict
1205
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1206
return sorted(conflict_iter, key=Conflict.sort_key)
1209
def iter_cook_conflicts(raw_conflicts, tt):
1210
from bzrlib.conflicts import Conflict
1212
for conflict in raw_conflicts:
1213
c_type = conflict[0]
1214
action = conflict[1]
1215
modified_path = fp.get_path(conflict[2])
1216
modified_id = tt.final_file_id(conflict[2])
1217
if len(conflict) == 3:
1218
yield Conflict.factory(c_type, action=action, path=modified_path,
1219
file_id=modified_id)
1222
conflicting_path = fp.get_path(conflict[3])
1223
conflicting_id = tt.final_file_id(conflict[3])
1224
yield Conflict.factory(c_type, action=action, path=modified_path,
1225
file_id=modified_id,
1226
conflict_path=conflicting_path,
1227
conflict_file_id=conflicting_id)