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
from bzrlib.progress import DummyProgress, ProgressPhase
27
from bzrlib.trace import mutter, warning
31
ROOT_PARENT = "root-parent"
34
def unique_add(map, key, value):
36
raise DuplicateKey(key=key)
40
class _TransformResults(object):
41
def __init__(self, modified_paths):
43
self.modified_paths = modified_paths
46
class TreeTransform(object):
47
"""Represent a tree transformation.
49
This object is designed to support incremental generation of the transform,
52
It is easy to produce malformed transforms, but they are generally
53
harmless. Attempting to apply a malformed transform will cause an
54
exception to be raised before any modifications are made to the tree.
56
Many kinds of malformed transforms can be corrected with the
57
resolve_conflicts function. The remaining ones indicate programming error,
58
such as trying to create a file with no path.
60
Two sets of file creation methods are supplied. Convenience methods are:
65
These are composed of the low-level methods:
67
* create_file or create_directory or create_symlink
71
def __init__(self, tree, pb=DummyProgress()):
72
"""Note: a write lock is taken on the tree.
74
Use TreeTransform.finalize() to release the lock
78
self._tree.lock_write()
80
control_files = self._tree._control_files
81
self._limbodir = control_files.controlfilename('limbo')
83
os.mkdir(self._limbodir)
85
if e.errno == errno.EEXIST:
86
raise ExistingLimbo(self._limbodir)
94
self._new_contents = {}
95
self._removed_contents = set()
96
self._new_executability = {}
98
self._non_present_ids = {}
100
self._removed_id = set()
101
self._tree_path_ids = {}
102
self._tree_id_paths = {}
103
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
107
def __get_root(self):
108
return self._new_root
110
root = property(__get_root)
113
"""Release the working tree lock, if held, clean up limbo dir."""
114
if self._tree is None:
117
for trans_id, kind in self._new_contents.iteritems():
118
path = self._limbo_name(trans_id)
119
if kind == "directory":
124
os.rmdir(self._limbodir)
126
# We don't especially care *why* the dir is immortal.
127
raise ImmortalLimbo(self._limbodir)
132
def _assign_id(self):
133
"""Produce a new tranform id"""
134
new_id = "new-%s" % self._id_number
138
def create_path(self, name, parent):
139
"""Assign a transaction id to a new path"""
140
trans_id = self._assign_id()
141
unique_add(self._new_name, trans_id, name)
142
unique_add(self._new_parent, trans_id, parent)
145
def adjust_path(self, name, parent, trans_id):
146
"""Change the path that is assigned to a transaction id."""
147
if trans_id == self._new_root:
149
self._new_name[trans_id] = name
150
self._new_parent[trans_id] = parent
152
def adjust_root_path(self, name, parent):
153
"""Emulate moving the root by moving all children, instead.
155
We do this by undoing the association of root's transaction id with the
156
current tree. This allows us to create a new directory with that
157
transaction id. We unversion the root directory and version the
158
physically new directory, and hope someone versions the tree root
161
old_root = self._new_root
162
old_root_file_id = self.final_file_id(old_root)
163
# force moving all children of root
164
for child_id in self.iter_tree_children(old_root):
165
if child_id != parent:
166
self.adjust_path(self.final_name(child_id),
167
self.final_parent(child_id), child_id)
168
file_id = self.final_file_id(child_id)
169
if file_id is not None:
170
self.unversion_file(child_id)
171
self.version_file(file_id, child_id)
173
# the physical root needs a new transaction id
174
self._tree_path_ids.pop("")
175
self._tree_id_paths.pop(old_root)
176
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
177
if parent == old_root:
178
parent = self._new_root
179
self.adjust_path(name, parent, old_root)
180
self.create_directory(old_root)
181
self.version_file(old_root_file_id, old_root)
182
self.unversion_file(self._new_root)
184
def trans_id_tree_file_id(self, inventory_id):
185
"""Determine the transaction id of a working tree file.
187
This reflects only files that already exist, not ones that will be
188
added by transactions.
190
path = self._tree.inventory.id2path(inventory_id)
191
return self.trans_id_tree_path(path)
193
def trans_id_file_id(self, file_id):
194
"""Determine or set the transaction id associated with a file ID.
195
A new id is only created for file_ids that were never present. If
196
a transaction has been unversioned, it is deliberately still returned.
197
(this will likely lead to an unversioned parent conflict.)
199
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
200
return self._r_new_id[file_id]
201
elif file_id in self._tree.inventory:
202
return self.trans_id_tree_file_id(file_id)
203
elif file_id in self._non_present_ids:
204
return self._non_present_ids[file_id]
206
trans_id = self._assign_id()
207
self._non_present_ids[file_id] = trans_id
210
def canonical_path(self, path):
211
"""Get the canonical tree-relative path"""
212
# don't follow final symlinks
213
dirname, basename = os.path.split(self._tree.abspath(path))
214
dirname = os.path.realpath(dirname)
215
return self._tree.relpath(pathjoin(dirname, basename))
217
def trans_id_tree_path(self, path):
218
"""Determine (and maybe set) the transaction ID for a tree path."""
219
path = self.canonical_path(path)
220
if path not in self._tree_path_ids:
221
self._tree_path_ids[path] = self._assign_id()
222
self._tree_id_paths[self._tree_path_ids[path]] = path
223
return self._tree_path_ids[path]
225
def get_tree_parent(self, trans_id):
226
"""Determine id of the parent in the tree."""
227
path = self._tree_id_paths[trans_id]
230
return self.trans_id_tree_path(os.path.dirname(path))
232
def create_file(self, contents, trans_id, mode_id=None):
233
"""Schedule creation of a new file.
237
Contents is an iterator of strings, all of which will be written
238
to the target destination.
240
New file takes the permissions of any existing file with that id,
241
unless mode_id is specified.
243
f = file(self._limbo_name(trans_id), 'wb')
244
unique_add(self._new_contents, trans_id, 'file')
245
for segment in contents:
248
self._set_mode(trans_id, mode_id, S_ISREG)
250
def _set_mode(self, trans_id, mode_id, typefunc):
251
"""Set the mode of new file contents.
252
The mode_id is the existing file to get the mode from (often the same
253
as trans_id). The operation is only performed if there's a mode match
254
according to typefunc.
259
old_path = self._tree_id_paths[mode_id]
263
mode = os.stat(old_path).st_mode
265
if e.errno == errno.ENOENT:
270
os.chmod(self._limbo_name(trans_id), mode)
272
def create_directory(self, trans_id):
273
"""Schedule creation of a new directory.
275
See also new_directory.
277
os.mkdir(self._limbo_name(trans_id))
278
unique_add(self._new_contents, trans_id, 'directory')
280
def create_symlink(self, target, trans_id):
281
"""Schedule creation of a new symbolic link.
283
target is a bytestring.
284
See also new_symlink.
286
os.symlink(target, self._limbo_name(trans_id))
287
unique_add(self._new_contents, trans_id, 'symlink')
290
def delete_any(full_path):
291
"""Delete a file or directory."""
295
# We may be renaming a dangling inventory id
296
if e.errno not in (errno.EISDIR, errno.EACCES, errno.EPERM):
300
def cancel_creation(self, trans_id):
301
"""Cancel the creation of new file contents."""
302
del self._new_contents[trans_id]
303
self.delete_any(self._limbo_name(trans_id))
305
def delete_contents(self, trans_id):
306
"""Schedule the contents of a path entry for deletion"""
307
self.tree_kind(trans_id)
308
self._removed_contents.add(trans_id)
310
def cancel_deletion(self, trans_id):
311
"""Cancel a scheduled deletion"""
312
self._removed_contents.remove(trans_id)
314
def unversion_file(self, trans_id):
315
"""Schedule a path entry to become unversioned"""
316
self._removed_id.add(trans_id)
318
def delete_versioned(self, trans_id):
319
"""Delete and unversion a versioned file"""
320
self.delete_contents(trans_id)
321
self.unversion_file(trans_id)
323
def set_executability(self, executability, trans_id):
324
"""Schedule setting of the 'execute' bit
325
To unschedule, set to None
327
if executability is None:
328
del self._new_executability[trans_id]
330
unique_add(self._new_executability, trans_id, executability)
332
def version_file(self, file_id, trans_id):
333
"""Schedule a file to become versioned."""
334
assert file_id is not None
335
unique_add(self._new_id, trans_id, file_id)
336
unique_add(self._r_new_id, file_id, trans_id)
338
def cancel_versioning(self, trans_id):
339
"""Undo a previous versioning of a file"""
340
file_id = self._new_id[trans_id]
341
del self._new_id[trans_id]
342
del self._r_new_id[file_id]
345
"""Determine the paths of all new and changed files"""
347
fp = FinalPaths(self)
348
for id_set in (self._new_name, self._new_parent, self._new_contents,
349
self._new_id, self._new_executability):
350
new_ids.update(id_set)
351
new_paths = [(fp.get_path(t), t) for t in new_ids]
355
def tree_kind(self, trans_id):
356
"""Determine the file kind in the working tree.
358
Raises NoSuchFile if the file does not exist
360
path = self._tree_id_paths.get(trans_id)
362
raise NoSuchFile(None)
364
return file_kind(self._tree.abspath(path))
366
if e.errno != errno.ENOENT:
369
raise NoSuchFile(path)
371
def final_kind(self, trans_id):
372
"""Determine the final file kind, after any changes applied.
374
Raises NoSuchFile if the file does not exist/has no contents.
375
(It is conceivable that a path would be created without the
376
corresponding contents insertion command)
378
if trans_id in self._new_contents:
379
return self._new_contents[trans_id]
380
elif trans_id in self._removed_contents:
381
raise NoSuchFile(None)
383
return self.tree_kind(trans_id)
385
def tree_file_id(self, trans_id):
386
"""Determine the file id associated with the trans_id in the tree"""
388
path = self._tree_id_paths[trans_id]
390
# the file is a new, unversioned file, or invalid trans_id
392
# the file is old; the old id is still valid
393
if self._new_root == trans_id:
394
return self._tree.inventory.root.file_id
395
return self._tree.inventory.path2id(path)
397
def final_file_id(self, trans_id):
398
"""Determine the file id after any changes are applied, or None.
400
None indicates that the file will not be versioned after changes are
404
# there is a new id for this file
405
assert self._new_id[trans_id] is not None
406
return self._new_id[trans_id]
408
if trans_id in self._removed_id:
410
return self.tree_file_id(trans_id)
412
def inactive_file_id(self, trans_id):
413
"""Return the inactive file_id associated with a transaction id.
414
That is, the one in the tree or in non_present_ids.
415
The file_id may actually be active, too.
417
file_id = self.tree_file_id(trans_id)
418
if file_id is not None:
420
for key, value in self._non_present_ids.iteritems():
421
if value == trans_id:
424
def final_parent(self, trans_id):
425
"""Determine the parent file_id, after any changes are applied.
427
ROOT_PARENT is returned for the tree root.
430
return self._new_parent[trans_id]
432
return self.get_tree_parent(trans_id)
434
def final_name(self, trans_id):
435
"""Determine the final filename, after all changes are applied."""
437
return self._new_name[trans_id]
439
return os.path.basename(self._tree_id_paths[trans_id])
442
"""Return a map of parent: children for known parents.
444
Only new paths and parents of tree files with assigned ids are used.
447
items = list(self._new_parent.iteritems())
448
items.extend((t, self.final_parent(t)) for t in
449
self._tree_id_paths.keys())
450
for trans_id, parent_id in items:
451
if parent_id not in by_parent:
452
by_parent[parent_id] = set()
453
by_parent[parent_id].add(trans_id)
456
def path_changed(self, trans_id):
457
"""Return True if a trans_id's path has changed."""
458
return trans_id in self._new_name or trans_id in self._new_parent
460
def find_conflicts(self):
461
"""Find any violations of inventory or filesystem invariants"""
462
if self.__done is True:
463
raise ReusingTransform()
465
# ensure all children of all existent parents are known
466
# all children of non-existent parents are known, by definition.
467
self._add_tree_children()
468
by_parent = self.by_parent()
469
conflicts.extend(self._unversioned_parents(by_parent))
470
conflicts.extend(self._parent_loops())
471
conflicts.extend(self._duplicate_entries(by_parent))
472
conflicts.extend(self._duplicate_ids())
473
conflicts.extend(self._parent_type_conflicts(by_parent))
474
conflicts.extend(self._improper_versioning())
475
conflicts.extend(self._executability_conflicts())
476
conflicts.extend(self._overwrite_conflicts())
479
def _add_tree_children(self):
480
"""Add all the children of all active parents to the known paths.
482
Active parents are those which gain children, and those which are
483
removed. This is a necessary first step in detecting conflicts.
485
parents = self.by_parent().keys()
486
parents.extend([t for t in self._removed_contents if
487
self.tree_kind(t) == 'directory'])
488
for trans_id in self._removed_id:
489
file_id = self.tree_file_id(trans_id)
490
if self._tree.inventory[file_id].kind in ('directory',
492
parents.append(trans_id)
494
for parent_id in parents:
495
# ensure that all children are registered with the transaction
496
list(self.iter_tree_children(parent_id))
498
def iter_tree_children(self, parent_id):
499
"""Iterate through the entry's tree children, if any"""
501
path = self._tree_id_paths[parent_id]
505
children = os.listdir(self._tree.abspath(path))
507
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
511
for child in children:
512
childpath = joinpath(path, child)
513
if self._tree.is_control_filename(childpath):
515
yield self.trans_id_tree_path(childpath)
517
def has_named_child(self, by_parent, parent_id, name):
519
children = by_parent[parent_id]
522
for child in children:
523
if self.final_name(child) == name:
526
path = self._tree_id_paths[parent_id]
529
childpath = joinpath(path, name)
530
child_id = self._tree_path_ids.get(childpath)
532
return lexists(self._tree.abspath(childpath))
534
if tt.final_parent(child_id) != parent_id:
536
if child_id in tt._removed_contents:
537
# XXX What about dangling file-ids?
542
def _parent_loops(self):
543
"""No entry should be its own ancestor"""
545
for trans_id in self._new_parent:
548
while parent_id is not ROOT_PARENT:
550
parent_id = self.final_parent(parent_id)
551
if parent_id == trans_id:
552
conflicts.append(('parent loop', trans_id))
553
if parent_id in seen:
557
def _unversioned_parents(self, by_parent):
558
"""If parent directories are versioned, children must be versioned."""
560
for parent_id, children in by_parent.iteritems():
561
if parent_id is ROOT_PARENT:
563
if self.final_file_id(parent_id) is not None:
565
for child_id in children:
566
if self.final_file_id(child_id) is not None:
567
conflicts.append(('unversioned parent', parent_id))
571
def _improper_versioning(self):
572
"""Cannot version a file with no contents, or a bad type.
574
However, existing entries with no contents are okay.
577
for trans_id in self._new_id.iterkeys():
579
kind = self.final_kind(trans_id)
581
conflicts.append(('versioning no contents', trans_id))
583
if not InventoryEntry.versionable_kind(kind):
584
conflicts.append(('versioning bad kind', trans_id, kind))
587
def _executability_conflicts(self):
588
"""Check for bad executability changes.
590
Only versioned files may have their executability set, because
591
1. only versioned entries can have executability under windows
592
2. only files can be executable. (The execute bit on a directory
593
does not indicate searchability)
596
for trans_id in self._new_executability:
597
if self.final_file_id(trans_id) is None:
598
conflicts.append(('unversioned executability', trans_id))
601
non_file = self.final_kind(trans_id) != "file"
605
conflicts.append(('non-file executability', trans_id))
608
def _overwrite_conflicts(self):
609
"""Check for overwrites (not permitted on Win32)"""
611
for trans_id in self._new_contents:
613
self.tree_kind(trans_id)
616
if trans_id not in self._removed_contents:
617
conflicts.append(('overwrite', trans_id,
618
self.final_name(trans_id)))
621
def _duplicate_entries(self, by_parent):
622
"""No directory may have two entries with the same name."""
624
for children in by_parent.itervalues():
625
name_ids = [(self.final_name(t), t) for t in children]
629
for name, trans_id in name_ids:
630
if name == last_name:
631
conflicts.append(('duplicate', last_trans_id, trans_id,
634
kind = self.final_kind(trans_id)
637
file_id = self.final_file_id(trans_id)
638
if kind is not None or file_id is not None:
640
last_trans_id = trans_id
643
def _duplicate_ids(self):
644
"""Each inventory id may only be used once"""
646
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
648
active_tree_ids = set((f for f in self._tree.inventory if
649
f not in removed_tree_ids))
650
for trans_id, file_id in self._new_id.iteritems():
651
if file_id in active_tree_ids:
652
old_trans_id = self.trans_id_tree_file_id(file_id)
653
conflicts.append(('duplicate id', old_trans_id, trans_id))
656
def _parent_type_conflicts(self, by_parent):
657
"""parents must have directory 'contents'."""
659
for parent_id, children in by_parent.iteritems():
660
if parent_id is ROOT_PARENT:
662
if not self._any_contents(children):
664
for child in children:
666
self.final_kind(child)
670
kind = self.final_kind(parent_id)
674
conflicts.append(('missing parent', parent_id))
675
elif kind != "directory":
676
conflicts.append(('non-directory parent', parent_id))
679
def _any_contents(self, trans_ids):
680
"""Return true if any of the trans_ids, will have contents."""
681
for trans_id in trans_ids:
683
kind = self.final_kind(trans_id)
690
"""Apply all changes to the inventory and filesystem.
692
If filesystem or inventory conflicts are present, MalformedTransform
695
conflicts = self.find_conflicts()
696
if len(conflicts) != 0:
697
raise MalformedTransform(conflicts=conflicts)
699
inv = self._tree.inventory
700
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
702
child_pb.update('Apply phase', 0, 2)
703
self._apply_removals(inv, limbo_inv)
704
child_pb.update('Apply phase', 1, 2)
705
modified_paths = self._apply_insertions(inv, limbo_inv)
708
self._tree._write_inventory(inv)
711
return _TransformResults(modified_paths)
713
def _limbo_name(self, trans_id):
714
"""Generate the limbo name of a file"""
715
return pathjoin(self._limbodir, trans_id)
717
def _apply_removals(self, inv, limbo_inv):
718
"""Perform tree operations that remove directory/inventory names.
720
That is, delete files that are to be deleted, and put any files that
721
need renaming into limbo. This must be done in strict child-to-parent
724
tree_paths = list(self._tree_path_ids.iteritems())
725
tree_paths.sort(reverse=True)
726
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
728
for num, data in enumerate(tree_paths):
729
path, trans_id = data
730
child_pb.update('removing file', num, len(tree_paths))
731
full_path = self._tree.abspath(path)
732
if trans_id in self._removed_contents:
733
self.delete_any(full_path)
734
elif trans_id in self._new_name or trans_id in \
737
os.rename(full_path, self._limbo_name(trans_id))
739
if e.errno != errno.ENOENT:
741
if trans_id in self._removed_id:
742
if trans_id == self._new_root:
743
file_id = self._tree.inventory.root.file_id
745
file_id = self.tree_file_id(trans_id)
747
elif trans_id in self._new_name or trans_id in self._new_parent:
748
file_id = self.tree_file_id(trans_id)
749
if file_id is not None:
750
limbo_inv[trans_id] = inv[file_id]
755
def _apply_insertions(self, inv, limbo_inv):
756
"""Perform tree operations that insert directory/inventory names.
758
That is, create any files that need to be created, and restore from
759
limbo any files that needed renaming. This must be done in strict
760
parent-to-child order.
762
new_paths = self.new_paths()
764
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
766
for num, (path, trans_id) in enumerate(new_paths):
767
child_pb.update('adding file', num, len(new_paths))
769
kind = self._new_contents[trans_id]
771
kind = contents = None
772
if trans_id in self._new_contents or \
773
self.path_changed(trans_id):
774
full_path = self._tree.abspath(path)
776
os.rename(self._limbo_name(trans_id), full_path)
778
# We may be renaming a dangling inventory id
779
if e.errno != errno.ENOENT:
781
if trans_id in self._new_contents:
782
modified_paths.append(full_path)
783
del self._new_contents[trans_id]
785
if trans_id in self._new_id:
787
kind = file_kind(self._tree.abspath(path))
788
inv.add_path(path, kind, self._new_id[trans_id])
789
elif trans_id in self._new_name or trans_id in\
791
entry = limbo_inv.get(trans_id)
792
if entry is not None:
793
entry.name = self.final_name(trans_id)
794
parent_path = os.path.dirname(path)
796
self._tree.inventory.path2id(parent_path)
799
# requires files and inventory entries to be in place
800
if trans_id in self._new_executability:
801
self._set_executability(path, inv, trans_id)
804
return modified_paths
806
def _set_executability(self, path, inv, trans_id):
807
"""Set the executability of versioned files """
808
file_id = inv.path2id(path)
809
new_executability = self._new_executability[trans_id]
810
inv[file_id].executable = new_executability
811
if supports_executable():
812
abspath = self._tree.abspath(path)
813
current_mode = os.stat(abspath).st_mode
814
if new_executability:
817
to_mode = current_mode | (0100 & ~umask)
818
# Enable x-bit for others only if they can read it.
819
if current_mode & 0004:
820
to_mode |= 0001 & ~umask
821
if current_mode & 0040:
822
to_mode |= 0010 & ~umask
824
to_mode = current_mode & ~0111
825
os.chmod(abspath, to_mode)
827
def _new_entry(self, name, parent_id, file_id):
828
"""Helper function to create a new filesystem entry."""
829
trans_id = self.create_path(name, parent_id)
830
if file_id is not None:
831
self.version_file(file_id, trans_id)
834
def new_file(self, name, parent_id, contents, file_id=None,
836
"""Convenience method to create files.
838
name is the name of the file to create.
839
parent_id is the transaction id of the parent directory of the file.
840
contents is an iterator of bytestrings, which will be used to produce
842
file_id is the inventory ID of the file, if it is to be versioned.
844
trans_id = self._new_entry(name, parent_id, file_id)
845
self.create_file(contents, trans_id)
846
if executable is not None:
847
self.set_executability(executable, trans_id)
850
def new_directory(self, name, parent_id, file_id=None):
851
"""Convenience method to create directories.
853
name is the name of the directory to create.
854
parent_id is the transaction id of the parent directory of the
856
file_id is the inventory ID of the directory, if it is to be versioned.
858
trans_id = self._new_entry(name, parent_id, file_id)
859
self.create_directory(trans_id)
862
def new_symlink(self, name, parent_id, target, file_id=None):
863
"""Convenience method to create symbolic link.
865
name is the name of the symlink to create.
866
parent_id is the transaction id of the parent directory of the symlink.
867
target is a bytestring of the target of the symlink.
868
file_id is the inventory ID of the file, if it is to be versioned.
870
trans_id = self._new_entry(name, parent_id, file_id)
871
self.create_symlink(target, trans_id)
874
def joinpath(parent, child):
875
"""Join tree-relative paths, handling the tree root specially"""
876
if parent is None or parent == "":
879
return pathjoin(parent, child)
882
class FinalPaths(object):
883
"""Make path calculation cheap by memoizing paths.
885
The underlying tree must not be manipulated between calls, or else
886
the results will likely be incorrect.
888
def __init__(self, transform):
889
object.__init__(self)
890
self._known_paths = {}
891
self.transform = transform
893
def _determine_path(self, trans_id):
894
if trans_id == self.transform.root:
896
name = self.transform.final_name(trans_id)
897
parent_id = self.transform.final_parent(trans_id)
898
if parent_id == self.transform.root:
901
return pathjoin(self.get_path(parent_id), name)
903
def get_path(self, trans_id):
904
"""Find the final path associated with a trans_id"""
905
if trans_id not in self._known_paths:
906
self._known_paths[trans_id] = self._determine_path(trans_id)
907
return self._known_paths[trans_id]
909
def topology_sorted_ids(tree):
910
"""Determine the topological order of the ids in a tree"""
911
file_ids = list(tree)
912
file_ids.sort(key=tree.id2path)
915
def build_tree(tree, wt):
916
"""Create working tree for a branch, using a Transaction."""
918
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
919
pp = ProgressPhase("Build phase", 2, top_pb)
920
tt = TreeTransform(wt)
923
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
924
file_ids = topology_sorted_ids(tree)
925
pb = bzrlib.ui.ui_factory.nested_progress_bar()
927
for num, file_id in enumerate(file_ids):
928
pb.update("Building tree", num, len(file_ids))
929
entry = tree.inventory[file_id]
930
if entry.parent_id is None:
932
if entry.parent_id not in file_trans_id:
933
raise repr(entry.parent_id)
934
parent_id = file_trans_id[entry.parent_id]
935
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
945
def new_by_entry(tt, entry, parent_id, tree):
946
"""Create a new file according to its inventory entry"""
950
contents = tree.get_file(entry.file_id).readlines()
951
executable = tree.is_executable(entry.file_id)
952
return tt.new_file(name, parent_id, contents, entry.file_id,
954
elif kind == 'directory':
955
return tt.new_directory(name, parent_id, entry.file_id)
956
elif kind == 'symlink':
957
target = tree.get_symlink_target(entry.file_id)
958
return tt.new_symlink(name, parent_id, target, entry.file_id)
960
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
961
"""Create new file contents according to an inventory entry."""
962
if entry.kind == "file":
964
lines = tree.get_file(entry.file_id).readlines()
965
tt.create_file(lines, trans_id, mode_id=mode_id)
966
elif entry.kind == "symlink":
967
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
968
elif entry.kind == "directory":
969
tt.create_directory(trans_id)
971
def create_entry_executability(tt, entry, trans_id):
972
"""Set the executability of a trans_id according to an inventory entry"""
973
if entry.kind == "file":
974
tt.set_executability(entry.executable, trans_id)
977
def find_interesting(working_tree, target_tree, filenames):
978
"""Find the ids corresponding to specified filenames."""
980
interesting_ids = None
982
interesting_ids = set()
983
for tree_path in filenames:
984
for tree in (working_tree, target_tree):
986
file_id = tree.inventory.path2id(tree_path)
987
if file_id is not None:
988
interesting_ids.add(file_id)
991
raise NotVersionedError(path=tree_path)
992
return interesting_ids
995
def change_entry(tt, file_id, working_tree, target_tree,
996
trans_id_file_id, backups, trans_id, by_parent):
997
"""Replace a file_id's contents with those from a target tree."""
998
e_trans_id = trans_id_file_id(file_id)
999
entry = target_tree.inventory[file_id]
1000
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1003
mode_id = e_trans_id
1006
tt.delete_contents(e_trans_id)
1008
parent_trans_id = trans_id_file_id(entry.parent_id)
1009
backup_name = get_backup_name(entry, by_parent,
1010
parent_trans_id, tt)
1011
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1012
tt.unversion_file(e_trans_id)
1013
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1014
tt.version_file(file_id, e_trans_id)
1015
trans_id[file_id] = e_trans_id
1016
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1017
create_entry_executability(tt, entry, e_trans_id)
1020
tt.set_executability(entry.executable, e_trans_id)
1021
if tt.final_name(e_trans_id) != entry.name:
1024
parent_id = tt.final_parent(e_trans_id)
1025
parent_file_id = tt.final_file_id(parent_id)
1026
if parent_file_id != entry.parent_id:
1031
parent_trans_id = trans_id_file_id(entry.parent_id)
1032
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1035
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1036
"""Produce a backup-style name that appears to be available"""
1040
yield "%s.~%d~" % (entry.name, counter)
1042
for name in name_gen():
1043
if not tt.has_named_child(by_parent, parent_trans_id, name):
1046
def _entry_changes(file_id, entry, working_tree):
1047
"""Determine in which ways the inventory entry has changed.
1049
Returns booleans: has_contents, content_mod, meta_mod
1050
has_contents means there are currently contents, but they differ
1051
contents_mod means contents need to be modified
1052
meta_mod means the metadata needs to be modified
1054
cur_entry = working_tree.inventory[file_id]
1056
working_kind = working_tree.kind(file_id)
1059
if e.errno != errno.ENOENT:
1061
has_contents = False
1064
if has_contents is True:
1065
real_e_kind = entry.kind
1066
if real_e_kind == 'root_directory':
1067
real_e_kind = 'directory'
1068
if real_e_kind != working_kind:
1069
contents_mod, meta_mod = True, False
1071
cur_entry._read_tree_state(working_tree.id2path(file_id),
1073
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1074
cur_entry._forget_tree_state()
1075
return has_contents, contents_mod, meta_mod
1078
def revert(working_tree, target_tree, filenames, backups=False,
1079
pb=DummyProgress()):
1080
"""Revert a working tree's contents to those of a target tree."""
1081
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1082
def interesting(file_id):
1083
return interesting_ids is None or file_id in interesting_ids
1085
tt = TreeTransform(working_tree, pb)
1087
merge_modified = working_tree.merge_modified()
1089
def trans_id_file_id(file_id):
1091
return trans_id[file_id]
1093
return tt.trans_id_tree_file_id(file_id)
1095
pp = ProgressPhase("Revert phase", 4, pb)
1097
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1099
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1101
by_parent = tt.by_parent()
1102
for id_num, file_id in enumerate(sorted_interesting):
1103
child_pb.update("Reverting file", id_num+1,
1104
len(sorted_interesting))
1105
if file_id not in working_tree.inventory:
1106
entry = target_tree.inventory[file_id]
1107
parent_id = trans_id_file_id(entry.parent_id)
1108
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1109
trans_id[file_id] = e_trans_id
1111
backup_this = backups
1112
if file_id in merge_modified:
1114
del merge_modified[file_id]
1115
change_entry(tt, file_id, working_tree, target_tree,
1116
trans_id_file_id, backup_this, trans_id,
1121
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1122
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1124
for id_num, file_id in enumerate(wt_interesting):
1125
child_pb.update("New file check", id_num+1,
1126
len(sorted_interesting))
1127
if file_id not in target_tree:
1128
trans_id = tt.trans_id_tree_file_id(file_id)
1129
tt.unversion_file(trans_id)
1130
if file_id in merge_modified:
1131
tt.delete_contents(trans_id)
1132
del merge_modified[file_id]
1136
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1138
raw_conflicts = resolve_conflicts(tt, child_pb)
1141
conflicts = cook_conflicts(raw_conflicts, tt)
1142
for conflict in conflicts:
1146
working_tree.set_merge_modified({})
1153
def resolve_conflicts(tt, pb=DummyProgress()):
1154
"""Make many conflict-resolution attempts, but die if they fail"""
1155
new_conflicts = set()
1158
pb.update('Resolution pass', n+1, 10)
1159
conflicts = tt.find_conflicts()
1160
if len(conflicts) == 0:
1161
return new_conflicts
1162
new_conflicts.update(conflict_pass(tt, conflicts))
1163
raise MalformedTransform(conflicts=conflicts)
1168
def conflict_pass(tt, conflicts):
1169
"""Resolve some classes of conflicts."""
1170
new_conflicts = set()
1171
for c_type, conflict in ((c[0], c) for c in conflicts):
1172
if c_type == 'duplicate id':
1173
tt.unversion_file(conflict[1])
1174
new_conflicts.add((c_type, 'Unversioned existing file',
1175
conflict[1], conflict[2], ))
1176
elif c_type == 'duplicate':
1177
# files that were renamed take precedence
1178
new_name = tt.final_name(conflict[1])+'.moved'
1179
final_parent = tt.final_parent(conflict[1])
1180
if tt.path_changed(conflict[1]):
1181
tt.adjust_path(new_name, final_parent, conflict[2])
1182
new_conflicts.add((c_type, 'Moved existing file to',
1183
conflict[2], conflict[1]))
1185
tt.adjust_path(new_name, final_parent, conflict[1])
1186
new_conflicts.add((c_type, 'Moved existing file to',
1187
conflict[1], conflict[2]))
1188
elif c_type == 'parent loop':
1189
# break the loop by undoing one of the ops that caused the loop
1191
while not tt.path_changed(cur):
1192
cur = tt.final_parent(cur)
1193
new_conflicts.add((c_type, 'Cancelled move', cur,
1194
tt.final_parent(cur),))
1195
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1197
elif c_type == 'missing parent':
1198
trans_id = conflict[1]
1200
tt.cancel_deletion(trans_id)
1201
new_conflicts.add((c_type, 'Not deleting', trans_id))
1203
tt.create_directory(trans_id)
1204
new_conflicts.add((c_type, 'Created directory.', trans_id))
1205
elif c_type == 'unversioned parent':
1206
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1207
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1208
return new_conflicts
1210
def cook_conflicts(raw_conflicts, tt):
1211
"""Generate a list of cooked conflicts, sorted by file path"""
1213
if conflict.path is not None:
1214
return conflict.path, conflict.typestring
1215
elif getattr(conflict, "conflict_path", None) is not None:
1216
return conflict.conflict_path, conflict.typestring
1218
return None, conflict.typestring
1220
return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1222
def iter_cook_conflicts(raw_conflicts, tt):
1223
from bzrlib.conflicts import Conflict
1225
for conflict in raw_conflicts:
1226
c_type = conflict[0]
1227
action = conflict[1]
1228
modified_path = fp.get_path(conflict[2])
1229
modified_id = tt.final_file_id(conflict[2])
1230
if len(conflict) == 3:
1231
yield Conflict.factory(c_type, action=action, path=modified_path,
1232
file_id=modified_id)
1235
conflicting_path = fp.get_path(conflict[3])
1236
conflicting_id = tt.final_file_id(conflict[3])
1237
yield Conflict.factory(c_type, action=action, path=modified_path,
1238
file_id=modified_id,
1239
conflict_path=conflicting_path,
1240
conflict_file_id=conflicting_id)