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
30
import bzrlib.urlutils as urlutils
33
ROOT_PARENT = "root-parent"
36
def unique_add(map, key, value):
38
raise DuplicateKey(key=key)
42
class _TransformResults(object):
43
def __init__(self, modified_paths):
45
self.modified_paths = modified_paths
48
class TreeTransform(object):
49
"""Represent a tree transformation.
51
This object is designed to support incremental generation of the transform,
54
It is easy to produce malformed transforms, but they are generally
55
harmless. Attempting to apply a malformed transform will cause an
56
exception to be raised before any modifications are made to the tree.
58
Many kinds of malformed transforms can be corrected with the
59
resolve_conflicts function. The remaining ones indicate programming error,
60
such as trying to create a file with no path.
62
Two sets of file creation methods are supplied. Convenience methods are:
67
These are composed of the low-level methods:
69
* create_file or create_directory or create_symlink
73
def __init__(self, tree, pb=DummyProgress()):
74
"""Note: a write lock is taken on the tree.
76
Use TreeTransform.finalize() to release the lock
80
self._tree.lock_write()
82
control_files = self._tree._control_files
83
self._limbodir = urlutils.local_path_from_url(
84
control_files.controlfilename('limbo'))
86
os.mkdir(self._limbodir)
88
if e.errno == errno.EEXIST:
89
raise ExistingLimbo(self._limbodir)
97
self._new_contents = {}
98
self._removed_contents = set()
99
self._new_executability = {}
101
self._non_present_ids = {}
103
self._removed_id = set()
104
self._tree_path_ids = {}
105
self._tree_id_paths = {}
107
# Cache of realpath results, to speed up canonical_path
109
# Cache of relpath results, to speed up canonical_path
110
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
114
def __get_root(self):
115
return self._new_root
117
root = property(__get_root)
120
"""Release the working tree lock, if held, clean up limbo dir."""
121
if self._tree is None:
124
for trans_id, kind in self._new_contents.iteritems():
125
path = self._limbo_name(trans_id)
126
if kind == "directory":
131
os.rmdir(self._limbodir)
133
# We don't especially care *why* the dir is immortal.
134
raise ImmortalLimbo(self._limbodir)
139
def _assign_id(self):
140
"""Produce a new tranform id"""
141
new_id = "new-%s" % self._id_number
145
def create_path(self, name, parent):
146
"""Assign a transaction id to a new path"""
147
trans_id = self._assign_id()
148
unique_add(self._new_name, trans_id, name)
149
unique_add(self._new_parent, trans_id, parent)
152
def adjust_path(self, name, parent, trans_id):
153
"""Change the path that is assigned to a transaction id."""
154
if trans_id == self._new_root:
156
self._new_name[trans_id] = name
157
self._new_parent[trans_id] = parent
159
def adjust_root_path(self, name, parent):
160
"""Emulate moving the root by moving all children, instead.
162
We do this by undoing the association of root's transaction id with the
163
current tree. This allows us to create a new directory with that
164
transaction id. We unversion the root directory and version the
165
physically new directory, and hope someone versions the tree root
168
old_root = self._new_root
169
old_root_file_id = self.final_file_id(old_root)
170
# force moving all children of root
171
for child_id in self.iter_tree_children(old_root):
172
if child_id != parent:
173
self.adjust_path(self.final_name(child_id),
174
self.final_parent(child_id), child_id)
175
file_id = self.final_file_id(child_id)
176
if file_id is not None:
177
self.unversion_file(child_id)
178
self.version_file(file_id, child_id)
180
# the physical root needs a new transaction id
181
self._tree_path_ids.pop("")
182
self._tree_id_paths.pop(old_root)
183
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
184
if parent == old_root:
185
parent = self._new_root
186
self.adjust_path(name, parent, old_root)
187
self.create_directory(old_root)
188
self.version_file(old_root_file_id, old_root)
189
self.unversion_file(self._new_root)
191
def trans_id_tree_file_id(self, inventory_id):
192
"""Determine the transaction id of a working tree file.
194
This reflects only files that already exist, not ones that will be
195
added by transactions.
197
path = self._tree.inventory.id2path(inventory_id)
198
return self.trans_id_tree_path(path)
200
def trans_id_file_id(self, file_id):
201
"""Determine or set the transaction id associated with a file ID.
202
A new id is only created for file_ids that were never present. If
203
a transaction has been unversioned, it is deliberately still returned.
204
(this will likely lead to an unversioned parent conflict.)
206
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
207
return self._r_new_id[file_id]
208
elif file_id in self._tree.inventory:
209
return self.trans_id_tree_file_id(file_id)
210
elif file_id in self._non_present_ids:
211
return self._non_present_ids[file_id]
213
trans_id = self._assign_id()
214
self._non_present_ids[file_id] = trans_id
217
def canonical_path(self, path):
218
"""Get the canonical tree-relative path"""
219
# don't follow final symlinks
220
abs = self._tree.abspath(path)
221
if abs in self._relpaths:
222
return self._relpaths[abs]
223
dirname, basename = os.path.split(abs)
224
if dirname not in self._realpaths:
225
self._realpaths[dirname] = os.path.realpath(dirname)
226
dirname = self._realpaths[dirname]
227
abs = pathjoin(dirname, basename)
228
if dirname in self._relpaths:
229
relpath = pathjoin(self._relpaths[dirname], basename)
230
relpath = relpath.rstrip('/\\')
232
relpath = self._tree.relpath(abs)
233
self._relpaths[abs] = relpath
236
def trans_id_tree_path(self, path):
237
"""Determine (and maybe set) the transaction ID for a tree path."""
238
path = self.canonical_path(path)
239
if path not in self._tree_path_ids:
240
self._tree_path_ids[path] = self._assign_id()
241
self._tree_id_paths[self._tree_path_ids[path]] = path
242
return self._tree_path_ids[path]
244
def get_tree_parent(self, trans_id):
245
"""Determine id of the parent in the tree."""
246
path = self._tree_id_paths[trans_id]
249
return self.trans_id_tree_path(os.path.dirname(path))
251
def create_file(self, contents, trans_id, mode_id=None):
252
"""Schedule creation of a new file.
256
Contents is an iterator of strings, all of which will be written
257
to the target destination.
259
New file takes the permissions of any existing file with that id,
260
unless mode_id is specified.
262
f = open(self._limbo_name(trans_id), 'wb')
264
unique_add(self._new_contents, trans_id, 'file')
265
for segment in contents:
269
self._set_mode(trans_id, mode_id, S_ISREG)
271
def _set_mode(self, trans_id, mode_id, typefunc):
272
"""Set the mode of new file contents.
273
The mode_id is the existing file to get the mode from (often the same
274
as trans_id). The operation is only performed if there's a mode match
275
according to typefunc.
280
old_path = self._tree_id_paths[mode_id]
284
mode = os.stat(old_path).st_mode
286
if e.errno == errno.ENOENT:
291
os.chmod(self._limbo_name(trans_id), mode)
293
def create_directory(self, trans_id):
294
"""Schedule creation of a new directory.
296
See also new_directory.
298
os.mkdir(self._limbo_name(trans_id))
299
unique_add(self._new_contents, trans_id, 'directory')
301
def create_symlink(self, target, trans_id):
302
"""Schedule creation of a new symbolic link.
304
target is a bytestring.
305
See also new_symlink.
307
os.symlink(target, self._limbo_name(trans_id))
308
unique_add(self._new_contents, trans_id, 'symlink')
310
def cancel_creation(self, trans_id):
311
"""Cancel the creation of new file contents."""
312
del self._new_contents[trans_id]
313
delete_any(self._limbo_name(trans_id))
315
def delete_contents(self, trans_id):
316
"""Schedule the contents of a path entry for deletion"""
317
self.tree_kind(trans_id)
318
self._removed_contents.add(trans_id)
320
def cancel_deletion(self, trans_id):
321
"""Cancel a scheduled deletion"""
322
self._removed_contents.remove(trans_id)
324
def unversion_file(self, trans_id):
325
"""Schedule a path entry to become unversioned"""
326
self._removed_id.add(trans_id)
328
def delete_versioned(self, trans_id):
329
"""Delete and unversion a versioned file"""
330
self.delete_contents(trans_id)
331
self.unversion_file(trans_id)
333
def set_executability(self, executability, trans_id):
334
"""Schedule setting of the 'execute' bit
335
To unschedule, set to None
337
if executability is None:
338
del self._new_executability[trans_id]
340
unique_add(self._new_executability, trans_id, executability)
342
def version_file(self, file_id, trans_id):
343
"""Schedule a file to become versioned."""
344
assert file_id is not None
345
unique_add(self._new_id, trans_id, file_id)
346
unique_add(self._r_new_id, file_id, trans_id)
348
def cancel_versioning(self, trans_id):
349
"""Undo a previous versioning of a file"""
350
file_id = self._new_id[trans_id]
351
del self._new_id[trans_id]
352
del self._r_new_id[file_id]
355
"""Determine the paths of all new and changed files"""
357
fp = FinalPaths(self)
358
for id_set in (self._new_name, self._new_parent, self._new_contents,
359
self._new_id, self._new_executability):
360
new_ids.update(id_set)
361
new_paths = [(fp.get_path(t), t) for t in new_ids]
365
def tree_kind(self, trans_id):
366
"""Determine the file kind in the working tree.
368
Raises NoSuchFile if the file does not exist
370
path = self._tree_id_paths.get(trans_id)
372
raise NoSuchFile(None)
374
return file_kind(self._tree.abspath(path))
376
if e.errno != errno.ENOENT:
379
raise NoSuchFile(path)
381
def final_kind(self, trans_id):
382
"""Determine the final file kind, after any changes applied.
384
Raises NoSuchFile if the file does not exist/has no contents.
385
(It is conceivable that a path would be created without the
386
corresponding contents insertion command)
388
if trans_id in self._new_contents:
389
return self._new_contents[trans_id]
390
elif trans_id in self._removed_contents:
391
raise NoSuchFile(None)
393
return self.tree_kind(trans_id)
395
def tree_file_id(self, trans_id):
396
"""Determine the file id associated with the trans_id in the tree"""
398
path = self._tree_id_paths[trans_id]
400
# the file is a new, unversioned file, or invalid trans_id
402
# the file is old; the old id is still valid
403
if self._new_root == trans_id:
404
return self._tree.inventory.root.file_id
405
return self._tree.inventory.path2id(path)
407
def final_file_id(self, trans_id):
408
"""Determine the file id after any changes are applied, or None.
410
None indicates that the file will not be versioned after changes are
414
# there is a new id for this file
415
assert self._new_id[trans_id] is not None
416
return self._new_id[trans_id]
418
if trans_id in self._removed_id:
420
return self.tree_file_id(trans_id)
422
def inactive_file_id(self, trans_id):
423
"""Return the inactive file_id associated with a transaction id.
424
That is, the one in the tree or in non_present_ids.
425
The file_id may actually be active, too.
427
file_id = self.tree_file_id(trans_id)
428
if file_id is not None:
430
for key, value in self._non_present_ids.iteritems():
431
if value == trans_id:
434
def final_parent(self, trans_id):
435
"""Determine the parent file_id, after any changes are applied.
437
ROOT_PARENT is returned for the tree root.
440
return self._new_parent[trans_id]
442
return self.get_tree_parent(trans_id)
444
def final_name(self, trans_id):
445
"""Determine the final filename, after all changes are applied."""
447
return self._new_name[trans_id]
449
return os.path.basename(self._tree_id_paths[trans_id])
452
"""Return a map of parent: children for known parents.
454
Only new paths and parents of tree files with assigned ids are used.
457
items = list(self._new_parent.iteritems())
458
items.extend((t, self.final_parent(t)) for t in
459
self._tree_id_paths.keys())
460
for trans_id, parent_id in items:
461
if parent_id not in by_parent:
462
by_parent[parent_id] = set()
463
by_parent[parent_id].add(trans_id)
466
def path_changed(self, trans_id):
467
"""Return True if a trans_id's path has changed."""
468
return trans_id in self._new_name or trans_id in self._new_parent
470
def find_conflicts(self):
471
"""Find any violations of inventory or filesystem invariants"""
472
if self.__done is True:
473
raise ReusingTransform()
475
# ensure all children of all existent parents are known
476
# all children of non-existent parents are known, by definition.
477
self._add_tree_children()
478
by_parent = self.by_parent()
479
conflicts.extend(self._unversioned_parents(by_parent))
480
conflicts.extend(self._parent_loops())
481
conflicts.extend(self._duplicate_entries(by_parent))
482
conflicts.extend(self._duplicate_ids())
483
conflicts.extend(self._parent_type_conflicts(by_parent))
484
conflicts.extend(self._improper_versioning())
485
conflicts.extend(self._executability_conflicts())
486
conflicts.extend(self._overwrite_conflicts())
489
def _add_tree_children(self):
490
"""Add all the children of all active parents to the known paths.
492
Active parents are those which gain children, and those which are
493
removed. This is a necessary first step in detecting conflicts.
495
parents = self.by_parent().keys()
496
parents.extend([t for t in self._removed_contents if
497
self.tree_kind(t) == 'directory'])
498
for trans_id in self._removed_id:
499
file_id = self.tree_file_id(trans_id)
500
if self._tree.inventory[file_id].kind in ('directory',
502
parents.append(trans_id)
504
for parent_id in parents:
505
# ensure that all children are registered with the transaction
506
list(self.iter_tree_children(parent_id))
508
def iter_tree_children(self, parent_id):
509
"""Iterate through the entry's tree children, if any"""
511
path = self._tree_id_paths[parent_id]
515
children = os.listdir(self._tree.abspath(path))
517
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
521
for child in children:
522
childpath = joinpath(path, child)
523
if self._tree.is_control_filename(childpath):
525
yield self.trans_id_tree_path(childpath)
527
def has_named_child(self, by_parent, parent_id, name):
529
children = by_parent[parent_id]
532
for child in children:
533
if self.final_name(child) == name:
536
path = self._tree_id_paths[parent_id]
539
childpath = joinpath(path, name)
540
child_id = self._tree_path_ids.get(childpath)
542
return lexists(self._tree.abspath(childpath))
544
if self.final_parent(child_id) != parent_id:
546
if child_id in self._removed_contents:
547
# XXX What about dangling file-ids?
552
def _parent_loops(self):
553
"""No entry should be its own ancestor"""
555
for trans_id in self._new_parent:
558
while parent_id is not ROOT_PARENT:
560
parent_id = self.final_parent(parent_id)
561
if parent_id == trans_id:
562
conflicts.append(('parent loop', trans_id))
563
if parent_id in seen:
567
def _unversioned_parents(self, by_parent):
568
"""If parent directories are versioned, children must be versioned."""
570
for parent_id, children in by_parent.iteritems():
571
if parent_id is ROOT_PARENT:
573
if self.final_file_id(parent_id) is not None:
575
for child_id in children:
576
if self.final_file_id(child_id) is not None:
577
conflicts.append(('unversioned parent', parent_id))
581
def _improper_versioning(self):
582
"""Cannot version a file with no contents, or a bad type.
584
However, existing entries with no contents are okay.
587
for trans_id in self._new_id.iterkeys():
589
kind = self.final_kind(trans_id)
591
conflicts.append(('versioning no contents', trans_id))
593
if not InventoryEntry.versionable_kind(kind):
594
conflicts.append(('versioning bad kind', trans_id, kind))
597
def _executability_conflicts(self):
598
"""Check for bad executability changes.
600
Only versioned files may have their executability set, because
601
1. only versioned entries can have executability under windows
602
2. only files can be executable. (The execute bit on a directory
603
does not indicate searchability)
606
for trans_id in self._new_executability:
607
if self.final_file_id(trans_id) is None:
608
conflicts.append(('unversioned executability', trans_id))
611
non_file = self.final_kind(trans_id) != "file"
615
conflicts.append(('non-file executability', trans_id))
618
def _overwrite_conflicts(self):
619
"""Check for overwrites (not permitted on Win32)"""
621
for trans_id in self._new_contents:
623
self.tree_kind(trans_id)
626
if trans_id not in self._removed_contents:
627
conflicts.append(('overwrite', trans_id,
628
self.final_name(trans_id)))
631
def _duplicate_entries(self, by_parent):
632
"""No directory may have two entries with the same name."""
634
for children in by_parent.itervalues():
635
name_ids = [(self.final_name(t), t) for t in children]
639
for name, trans_id in name_ids:
640
if name == last_name:
641
conflicts.append(('duplicate', last_trans_id, trans_id,
644
kind = self.final_kind(trans_id)
647
file_id = self.final_file_id(trans_id)
648
if kind is not None or file_id is not None:
650
last_trans_id = trans_id
653
def _duplicate_ids(self):
654
"""Each inventory id may only be used once"""
656
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
658
active_tree_ids = set((f for f in self._tree.inventory if
659
f not in removed_tree_ids))
660
for trans_id, file_id in self._new_id.iteritems():
661
if file_id in active_tree_ids:
662
old_trans_id = self.trans_id_tree_file_id(file_id)
663
conflicts.append(('duplicate id', old_trans_id, trans_id))
666
def _parent_type_conflicts(self, by_parent):
667
"""parents must have directory 'contents'."""
669
for parent_id, children in by_parent.iteritems():
670
if parent_id is ROOT_PARENT:
672
if not self._any_contents(children):
674
for child in children:
676
self.final_kind(child)
680
kind = self.final_kind(parent_id)
684
conflicts.append(('missing parent', parent_id))
685
elif kind != "directory":
686
conflicts.append(('non-directory parent', parent_id))
689
def _any_contents(self, trans_ids):
690
"""Return true if any of the trans_ids, will have contents."""
691
for trans_id in trans_ids:
693
kind = self.final_kind(trans_id)
700
"""Apply all changes to the inventory and filesystem.
702
If filesystem or inventory conflicts are present, MalformedTransform
705
conflicts = self.find_conflicts()
706
if len(conflicts) != 0:
707
raise MalformedTransform(conflicts=conflicts)
709
inv = self._tree.inventory
710
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
712
child_pb.update('Apply phase', 0, 2)
713
self._apply_removals(inv, limbo_inv)
714
child_pb.update('Apply phase', 1, 2)
715
modified_paths = self._apply_insertions(inv, limbo_inv)
718
self._tree._write_inventory(inv)
721
return _TransformResults(modified_paths)
723
def _limbo_name(self, trans_id):
724
"""Generate the limbo name of a file"""
725
return pathjoin(self._limbodir, trans_id)
727
def _apply_removals(self, inv, limbo_inv):
728
"""Perform tree operations that remove directory/inventory names.
730
That is, delete files that are to be deleted, and put any files that
731
need renaming into limbo. This must be done in strict child-to-parent
734
tree_paths = list(self._tree_path_ids.iteritems())
735
tree_paths.sort(reverse=True)
736
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
738
for num, data in enumerate(tree_paths):
739
path, trans_id = data
740
child_pb.update('removing file', num, len(tree_paths))
741
full_path = self._tree.abspath(path)
742
if trans_id in self._removed_contents:
743
delete_any(full_path)
744
elif trans_id in self._new_name or trans_id in \
747
os.rename(full_path, self._limbo_name(trans_id))
749
if e.errno != errno.ENOENT:
751
if trans_id in self._removed_id:
752
if trans_id == self._new_root:
753
file_id = self._tree.inventory.root.file_id
755
file_id = self.tree_file_id(trans_id)
757
elif trans_id in self._new_name or trans_id in self._new_parent:
758
file_id = self.tree_file_id(trans_id)
759
if file_id is not None:
760
limbo_inv[trans_id] = inv[file_id]
765
def _apply_insertions(self, inv, limbo_inv):
766
"""Perform tree operations that insert directory/inventory names.
768
That is, create any files that need to be created, and restore from
769
limbo any files that needed renaming. This must be done in strict
770
parent-to-child order.
772
new_paths = self.new_paths()
774
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
776
for num, (path, trans_id) in enumerate(new_paths):
777
child_pb.update('adding file', num, len(new_paths))
779
kind = self._new_contents[trans_id]
781
kind = contents = None
782
if trans_id in self._new_contents or \
783
self.path_changed(trans_id):
784
full_path = self._tree.abspath(path)
786
os.rename(self._limbo_name(trans_id), full_path)
788
# We may be renaming a dangling inventory id
789
if e.errno != errno.ENOENT:
791
if trans_id in self._new_contents:
792
modified_paths.append(full_path)
793
del self._new_contents[trans_id]
795
if trans_id in self._new_id:
797
kind = file_kind(self._tree.abspath(path))
798
inv.add_path(path, kind, self._new_id[trans_id])
799
elif trans_id in self._new_name or trans_id in\
801
entry = limbo_inv.get(trans_id)
802
if entry is not None:
803
entry.name = self.final_name(trans_id)
804
parent_path = os.path.dirname(path)
806
self._tree.inventory.path2id(parent_path)
809
# requires files and inventory entries to be in place
810
if trans_id in self._new_executability:
811
self._set_executability(path, inv, trans_id)
814
return modified_paths
816
def _set_executability(self, path, inv, trans_id):
817
"""Set the executability of versioned files """
818
file_id = inv.path2id(path)
819
new_executability = self._new_executability[trans_id]
820
inv[file_id].executable = new_executability
821
if supports_executable():
822
abspath = self._tree.abspath(path)
823
current_mode = os.stat(abspath).st_mode
824
if new_executability:
827
to_mode = current_mode | (0100 & ~umask)
828
# Enable x-bit for others only if they can read it.
829
if current_mode & 0004:
830
to_mode |= 0001 & ~umask
831
if current_mode & 0040:
832
to_mode |= 0010 & ~umask
834
to_mode = current_mode & ~0111
835
os.chmod(abspath, to_mode)
837
def _new_entry(self, name, parent_id, file_id):
838
"""Helper function to create a new filesystem entry."""
839
trans_id = self.create_path(name, parent_id)
840
if file_id is not None:
841
self.version_file(file_id, trans_id)
844
def new_file(self, name, parent_id, contents, file_id=None,
846
"""Convenience method to create files.
848
name is the name of the file to create.
849
parent_id is the transaction id of the parent directory of the file.
850
contents is an iterator of bytestrings, which will be used to produce
852
:param file_id: The inventory ID of the file, if it is to be versioned.
853
:param executable: Only valid when a file_id has been supplied.
855
trans_id = self._new_entry(name, parent_id, file_id)
856
# TODO: rather than scheduling a set_executable call,
857
# have create_file create the file with the right mode.
858
self.create_file(contents, trans_id)
859
if executable is not None:
860
self.set_executability(executable, trans_id)
863
def new_directory(self, name, parent_id, file_id=None):
864
"""Convenience method to create directories.
866
name is the name of the directory to create.
867
parent_id is the transaction id of the parent directory of the
869
file_id is the inventory ID of the directory, if it is to be versioned.
871
trans_id = self._new_entry(name, parent_id, file_id)
872
self.create_directory(trans_id)
875
def new_symlink(self, name, parent_id, target, file_id=None):
876
"""Convenience method to create symbolic link.
878
name is the name of the symlink to create.
879
parent_id is the transaction id of the parent directory of the symlink.
880
target is a bytestring of the target of the symlink.
881
file_id is the inventory ID of the file, if it is to be versioned.
883
trans_id = self._new_entry(name, parent_id, file_id)
884
self.create_symlink(target, trans_id)
887
def joinpath(parent, child):
888
"""Join tree-relative paths, handling the tree root specially"""
889
if parent is None or parent == "":
892
return pathjoin(parent, child)
895
class FinalPaths(object):
896
"""Make path calculation cheap by memoizing paths.
898
The underlying tree must not be manipulated between calls, or else
899
the results will likely be incorrect.
901
def __init__(self, transform):
902
object.__init__(self)
903
self._known_paths = {}
904
self.transform = transform
906
def _determine_path(self, trans_id):
907
if trans_id == self.transform.root:
909
name = self.transform.final_name(trans_id)
910
parent_id = self.transform.final_parent(trans_id)
911
if parent_id == self.transform.root:
914
return pathjoin(self.get_path(parent_id), name)
916
def get_path(self, trans_id):
917
"""Find the final path associated with a trans_id"""
918
if trans_id not in self._known_paths:
919
self._known_paths[trans_id] = self._determine_path(trans_id)
920
return self._known_paths[trans_id]
922
def topology_sorted_ids(tree):
923
"""Determine the topological order of the ids in a tree"""
924
file_ids = list(tree)
925
file_ids.sort(key=tree.id2path)
928
def build_tree(tree, wt):
929
"""Create working tree for a branch, using a Transaction."""
931
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
932
pp = ProgressPhase("Build phase", 2, top_pb)
933
tt = TreeTransform(wt)
936
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
937
file_ids = topology_sorted_ids(tree)
938
pb = bzrlib.ui.ui_factory.nested_progress_bar()
940
for num, file_id in enumerate(file_ids):
941
pb.update("Building tree", num, len(file_ids))
942
entry = tree.inventory[file_id]
943
if entry.parent_id is None:
945
if entry.parent_id not in file_trans_id:
946
raise repr(entry.parent_id)
947
parent_id = file_trans_id[entry.parent_id]
948
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
958
def new_by_entry(tt, entry, parent_id, tree):
959
"""Create a new file according to its inventory entry"""
963
contents = tree.get_file(entry.file_id).readlines()
964
executable = tree.is_executable(entry.file_id)
965
return tt.new_file(name, parent_id, contents, entry.file_id,
967
elif kind == 'directory':
968
return tt.new_directory(name, parent_id, entry.file_id)
969
elif kind == 'symlink':
970
target = tree.get_symlink_target(entry.file_id)
971
return tt.new_symlink(name, parent_id, target, entry.file_id)
973
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
974
"""Create new file contents according to an inventory entry."""
975
if entry.kind == "file":
977
lines = tree.get_file(entry.file_id).readlines()
978
tt.create_file(lines, trans_id, mode_id=mode_id)
979
elif entry.kind == "symlink":
980
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
981
elif entry.kind == "directory":
982
tt.create_directory(trans_id)
984
def create_entry_executability(tt, entry, trans_id):
985
"""Set the executability of a trans_id according to an inventory entry"""
986
if entry.kind == "file":
987
tt.set_executability(entry.executable, trans_id)
990
def find_interesting(working_tree, target_tree, filenames):
991
"""Find the ids corresponding to specified filenames."""
993
interesting_ids = None
995
interesting_ids = set()
996
for tree_path in filenames:
998
for tree in (working_tree, target_tree):
999
file_id = tree.inventory.path2id(tree_path)
1000
if file_id is not None:
1001
interesting_ids.add(file_id)
1004
raise NotVersionedError(path=tree_path)
1005
return interesting_ids
1008
def change_entry(tt, file_id, working_tree, target_tree,
1009
trans_id_file_id, backups, trans_id, by_parent):
1010
"""Replace a file_id's contents with those from a target tree."""
1011
e_trans_id = trans_id_file_id(file_id)
1012
entry = target_tree.inventory[file_id]
1013
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1016
mode_id = e_trans_id
1019
tt.delete_contents(e_trans_id)
1021
parent_trans_id = trans_id_file_id(entry.parent_id)
1022
backup_name = get_backup_name(entry, by_parent,
1023
parent_trans_id, tt)
1024
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1025
tt.unversion_file(e_trans_id)
1026
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1027
tt.version_file(file_id, e_trans_id)
1028
trans_id[file_id] = e_trans_id
1029
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1030
create_entry_executability(tt, entry, e_trans_id)
1033
tt.set_executability(entry.executable, e_trans_id)
1034
if tt.final_name(e_trans_id) != entry.name:
1037
parent_id = tt.final_parent(e_trans_id)
1038
parent_file_id = tt.final_file_id(parent_id)
1039
if parent_file_id != entry.parent_id:
1044
parent_trans_id = trans_id_file_id(entry.parent_id)
1045
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1048
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1049
"""Produce a backup-style name that appears to be available"""
1053
yield "%s.~%d~" % (entry.name, counter)
1055
for name in name_gen():
1056
if not tt.has_named_child(by_parent, parent_trans_id, name):
1059
def _entry_changes(file_id, entry, working_tree):
1060
"""Determine in which ways the inventory entry has changed.
1062
Returns booleans: has_contents, content_mod, meta_mod
1063
has_contents means there are currently contents, but they differ
1064
contents_mod means contents need to be modified
1065
meta_mod means the metadata needs to be modified
1067
cur_entry = working_tree.inventory[file_id]
1069
working_kind = working_tree.kind(file_id)
1072
has_contents = False
1075
if has_contents is True:
1076
real_e_kind = entry.kind
1077
if real_e_kind == 'root_directory':
1078
real_e_kind = 'directory'
1079
if real_e_kind != working_kind:
1080
contents_mod, meta_mod = True, False
1082
cur_entry._read_tree_state(working_tree.id2path(file_id),
1084
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1085
cur_entry._forget_tree_state()
1086
return has_contents, contents_mod, meta_mod
1089
def revert(working_tree, target_tree, filenames, backups=False,
1090
pb=DummyProgress()):
1091
"""Revert a working tree's contents to those of a target tree."""
1092
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1093
def interesting(file_id):
1094
return interesting_ids is None or file_id in interesting_ids
1096
tt = TreeTransform(working_tree, pb)
1098
merge_modified = working_tree.merge_modified()
1100
def trans_id_file_id(file_id):
1102
return trans_id[file_id]
1104
return tt.trans_id_tree_file_id(file_id)
1106
pp = ProgressPhase("Revert phase", 4, pb)
1108
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1110
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1112
by_parent = tt.by_parent()
1113
for id_num, file_id in enumerate(sorted_interesting):
1114
child_pb.update("Reverting file", id_num+1,
1115
len(sorted_interesting))
1116
if file_id not in working_tree.inventory:
1117
entry = target_tree.inventory[file_id]
1118
parent_id = trans_id_file_id(entry.parent_id)
1119
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1120
trans_id[file_id] = e_trans_id
1122
backup_this = backups
1123
if file_id in merge_modified:
1125
del merge_modified[file_id]
1126
change_entry(tt, file_id, working_tree, target_tree,
1127
trans_id_file_id, backup_this, trans_id,
1132
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1133
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1135
for id_num, file_id in enumerate(wt_interesting):
1136
child_pb.update("New file check", id_num+1,
1137
len(sorted_interesting))
1138
if file_id not in target_tree:
1139
trans_id = tt.trans_id_tree_file_id(file_id)
1140
tt.unversion_file(trans_id)
1141
if file_id in merge_modified:
1142
tt.delete_contents(trans_id)
1143
del merge_modified[file_id]
1147
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1149
raw_conflicts = resolve_conflicts(tt, child_pb)
1152
conflicts = cook_conflicts(raw_conflicts, tt)
1153
for conflict in conflicts:
1157
working_tree.set_merge_modified({})
1164
def resolve_conflicts(tt, pb=DummyProgress()):
1165
"""Make many conflict-resolution attempts, but die if they fail"""
1166
new_conflicts = set()
1169
pb.update('Resolution pass', n+1, 10)
1170
conflicts = tt.find_conflicts()
1171
if len(conflicts) == 0:
1172
return new_conflicts
1173
new_conflicts.update(conflict_pass(tt, conflicts))
1174
raise MalformedTransform(conflicts=conflicts)
1179
def conflict_pass(tt, conflicts):
1180
"""Resolve some classes of conflicts."""
1181
new_conflicts = set()
1182
for c_type, conflict in ((c[0], c) for c in conflicts):
1183
if c_type == 'duplicate id':
1184
tt.unversion_file(conflict[1])
1185
new_conflicts.add((c_type, 'Unversioned existing file',
1186
conflict[1], conflict[2], ))
1187
elif c_type == 'duplicate':
1188
# files that were renamed take precedence
1189
new_name = tt.final_name(conflict[1])+'.moved'
1190
final_parent = tt.final_parent(conflict[1])
1191
if tt.path_changed(conflict[1]):
1192
tt.adjust_path(new_name, final_parent, conflict[2])
1193
new_conflicts.add((c_type, 'Moved existing file to',
1194
conflict[2], conflict[1]))
1196
tt.adjust_path(new_name, final_parent, conflict[1])
1197
new_conflicts.add((c_type, 'Moved existing file to',
1198
conflict[1], conflict[2]))
1199
elif c_type == 'parent loop':
1200
# break the loop by undoing one of the ops that caused the loop
1202
while not tt.path_changed(cur):
1203
cur = tt.final_parent(cur)
1204
new_conflicts.add((c_type, 'Cancelled move', cur,
1205
tt.final_parent(cur),))
1206
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1208
elif c_type == 'missing parent':
1209
trans_id = conflict[1]
1211
tt.cancel_deletion(trans_id)
1212
new_conflicts.add((c_type, 'Not deleting', trans_id))
1214
tt.create_directory(trans_id)
1215
new_conflicts.add((c_type, 'Created directory.', trans_id))
1216
elif c_type == 'unversioned parent':
1217
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1218
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1219
return new_conflicts
1222
def cook_conflicts(raw_conflicts, tt):
1223
"""Generate a list of cooked conflicts, sorted by file path"""
1224
from bzrlib.conflicts import Conflict
1225
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1226
return sorted(conflict_iter, key=Conflict.sort_key)
1229
def iter_cook_conflicts(raw_conflicts, tt):
1230
from bzrlib.conflicts import Conflict
1232
for conflict in raw_conflicts:
1233
c_type = conflict[0]
1234
action = conflict[1]
1235
modified_path = fp.get_path(conflict[2])
1236
modified_id = tt.final_file_id(conflict[2])
1237
if len(conflict) == 3:
1238
yield Conflict.factory(c_type, action=action, path=modified_path,
1239
file_id=modified_id)
1242
conflicting_path = fp.get_path(conflict[3])
1243
conflicting_id = tt.final_file_id(conflict[3])
1244
yield Conflict.factory(c_type, action=action, path=modified_path,
1245
file_id=modified_id,
1246
conflict_path=conflicting_path,
1247
conflict_file_id=conflicting_id)