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
29
from bzrlib import tree
31
import bzrlib.urlutils as urlutils
34
ROOT_PARENT = "root-parent"
37
def unique_add(map, key, value):
39
raise DuplicateKey(key=key)
43
class _TransformResults(object):
44
def __init__(self, modified_paths):
46
self.modified_paths = modified_paths
49
class TreeTransform(object):
50
"""Represent a tree transformation.
52
This object is designed to support incremental generation of the transform,
55
It is easy to produce malformed transforms, but they are generally
56
harmless. Attempting to apply a malformed transform will cause an
57
exception to be raised before any modifications are made to the tree.
59
Many kinds of malformed transforms can be corrected with the
60
resolve_conflicts function. The remaining ones indicate programming error,
61
such as trying to create a file with no path.
63
Two sets of file creation methods are supplied. Convenience methods are:
68
These are composed of the low-level methods:
70
* create_file or create_directory or create_symlink
74
def __init__(self, tree, pb=DummyProgress()):
75
"""Note: a write lock is taken on the tree.
77
Use TreeTransform.finalize() to release the lock
81
self._tree.lock_write()
83
control_files = self._tree._control_files
84
self._limbodir = urlutils.local_path_from_url(
85
control_files.controlfilename('limbo'))
87
os.mkdir(self._limbodir)
89
if e.errno == errno.EEXIST:
90
raise ExistingLimbo(self._limbodir)
98
self._new_contents = {}
99
self._removed_contents = set()
100
self._new_executability = {}
102
self._non_present_ids = {}
104
self._removed_id = set()
105
self._tree_path_ids = {}
106
self._tree_id_paths = {}
108
# Cache of realpath results, to speed up canonical_path
110
# Cache of relpath results, to speed up canonical_path
111
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
115
def __get_root(self):
116
return self._new_root
118
root = property(__get_root)
121
"""Release the working tree lock, if held, clean up limbo dir."""
122
if self._tree is None:
125
for trans_id, kind in self._new_contents.iteritems():
126
path = self._limbo_name(trans_id)
127
if kind == "directory":
132
os.rmdir(self._limbodir)
134
# We don't especially care *why* the dir is immortal.
135
raise ImmortalLimbo(self._limbodir)
140
def _assign_id(self):
141
"""Produce a new tranform id"""
142
new_id = "new-%s" % self._id_number
146
def create_path(self, name, parent):
147
"""Assign a transaction id to a new path"""
148
trans_id = self._assign_id()
149
unique_add(self._new_name, trans_id, name)
150
unique_add(self._new_parent, trans_id, parent)
153
def adjust_path(self, name, parent, trans_id):
154
"""Change the path that is assigned to a transaction id."""
155
if trans_id == self._new_root:
157
self._new_name[trans_id] = name
158
self._new_parent[trans_id] = parent
160
def adjust_root_path(self, name, parent):
161
"""Emulate moving the root by moving all children, instead.
163
We do this by undoing the association of root's transaction id with the
164
current tree. This allows us to create a new directory with that
165
transaction id. We unversion the root directory and version the
166
physically new directory, and hope someone versions the tree root
169
old_root = self._new_root
170
old_root_file_id = self.final_file_id(old_root)
171
# force moving all children of root
172
for child_id in self.iter_tree_children(old_root):
173
if child_id != parent:
174
self.adjust_path(self.final_name(child_id),
175
self.final_parent(child_id), child_id)
176
file_id = self.final_file_id(child_id)
177
if file_id is not None:
178
self.unversion_file(child_id)
179
self.version_file(file_id, child_id)
181
# the physical root needs a new transaction id
182
self._tree_path_ids.pop("")
183
self._tree_id_paths.pop(old_root)
184
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
185
if parent == old_root:
186
parent = self._new_root
187
self.adjust_path(name, parent, old_root)
188
self.create_directory(old_root)
189
self.version_file(old_root_file_id, old_root)
190
self.unversion_file(self._new_root)
192
def trans_id_tree_file_id(self, inventory_id):
193
"""Determine the transaction id of a working tree file.
195
This reflects only files that already exist, not ones that will be
196
added by transactions.
198
path = self._tree.inventory.id2path(inventory_id)
199
return self.trans_id_tree_path(path)
201
def trans_id_file_id(self, file_id):
202
"""Determine or set the transaction id associated with a file ID.
203
A new id is only created for file_ids that were never present. If
204
a transaction has been unversioned, it is deliberately still returned.
205
(this will likely lead to an unversioned parent conflict.)
207
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
208
return self._r_new_id[file_id]
209
elif file_id in self._tree.inventory:
210
return self.trans_id_tree_file_id(file_id)
211
elif file_id in self._non_present_ids:
212
return self._non_present_ids[file_id]
214
trans_id = self._assign_id()
215
self._non_present_ids[file_id] = trans_id
218
def canonical_path(self, path):
219
"""Get the canonical tree-relative path"""
220
# don't follow final symlinks
221
abs = self._tree.abspath(path)
222
if abs in self._relpaths:
223
return self._relpaths[abs]
224
dirname, basename = os.path.split(abs)
225
if dirname not in self._realpaths:
226
self._realpaths[dirname] = os.path.realpath(dirname)
227
dirname = self._realpaths[dirname]
228
abs = pathjoin(dirname, basename)
229
if dirname in self._relpaths:
230
relpath = pathjoin(self._relpaths[dirname], basename)
231
relpath = relpath.rstrip('/\\')
233
relpath = self._tree.relpath(abs)
234
self._relpaths[abs] = relpath
237
def trans_id_tree_path(self, path):
238
"""Determine (and maybe set) the transaction ID for a tree path."""
239
path = self.canonical_path(path)
240
if path not in self._tree_path_ids:
241
self._tree_path_ids[path] = self._assign_id()
242
self._tree_id_paths[self._tree_path_ids[path]] = path
243
return self._tree_path_ids[path]
245
def get_tree_parent(self, trans_id):
246
"""Determine id of the parent in the tree."""
247
path = self._tree_id_paths[trans_id]
250
return self.trans_id_tree_path(os.path.dirname(path))
252
def create_file(self, contents, trans_id, mode_id=None):
253
"""Schedule creation of a new file.
257
Contents is an iterator of strings, all of which will be written
258
to the target destination.
260
New file takes the permissions of any existing file with that id,
261
unless mode_id is specified.
263
name = self._limbo_name(trans_id)
267
unique_add(self._new_contents, trans_id, 'file')
269
# Clean up the file, it never got registered so
270
# TreeTransform.finalize() won't clean it up.
275
for segment in contents:
279
self._set_mode(trans_id, mode_id, S_ISREG)
281
def _set_mode(self, trans_id, mode_id, typefunc):
282
"""Set the mode of new file contents.
283
The mode_id is the existing file to get the mode from (often the same
284
as trans_id). The operation is only performed if there's a mode match
285
according to typefunc.
290
old_path = self._tree_id_paths[mode_id]
294
mode = os.stat(old_path).st_mode
296
if e.errno == errno.ENOENT:
301
os.chmod(self._limbo_name(trans_id), mode)
303
def create_directory(self, trans_id):
304
"""Schedule creation of a new directory.
306
See also new_directory.
308
os.mkdir(self._limbo_name(trans_id))
309
unique_add(self._new_contents, trans_id, 'directory')
311
def create_symlink(self, target, trans_id):
312
"""Schedule creation of a new symbolic link.
314
target is a bytestring.
315
See also new_symlink.
317
os.symlink(target, self._limbo_name(trans_id))
318
unique_add(self._new_contents, trans_id, 'symlink')
320
def cancel_creation(self, trans_id):
321
"""Cancel the creation of new file contents."""
322
del self._new_contents[trans_id]
323
delete_any(self._limbo_name(trans_id))
325
def delete_contents(self, trans_id):
326
"""Schedule the contents of a path entry for deletion"""
327
self.tree_kind(trans_id)
328
self._removed_contents.add(trans_id)
330
def cancel_deletion(self, trans_id):
331
"""Cancel a scheduled deletion"""
332
self._removed_contents.remove(trans_id)
334
def unversion_file(self, trans_id):
335
"""Schedule a path entry to become unversioned"""
336
self._removed_id.add(trans_id)
338
def delete_versioned(self, trans_id):
339
"""Delete and unversion a versioned file"""
340
self.delete_contents(trans_id)
341
self.unversion_file(trans_id)
343
def set_executability(self, executability, trans_id):
344
"""Schedule setting of the 'execute' bit
345
To unschedule, set to None
347
if executability is None:
348
del self._new_executability[trans_id]
350
unique_add(self._new_executability, trans_id, executability)
352
def version_file(self, file_id, trans_id):
353
"""Schedule a file to become versioned."""
354
assert file_id is not None
355
unique_add(self._new_id, trans_id, file_id)
356
unique_add(self._r_new_id, file_id, trans_id)
358
def cancel_versioning(self, trans_id):
359
"""Undo a previous versioning of a file"""
360
file_id = self._new_id[trans_id]
361
del self._new_id[trans_id]
362
del self._r_new_id[file_id]
365
"""Determine the paths of all new and changed files"""
367
fp = FinalPaths(self)
368
for id_set in (self._new_name, self._new_parent, self._new_contents,
369
self._new_id, self._new_executability):
370
new_ids.update(id_set)
371
new_paths = [(fp.get_path(t), t) for t in new_ids]
375
def tree_kind(self, trans_id):
376
"""Determine the file kind in the working tree.
378
Raises NoSuchFile if the file does not exist
380
path = self._tree_id_paths.get(trans_id)
382
raise NoSuchFile(None)
384
return file_kind(self._tree.abspath(path))
386
if e.errno != errno.ENOENT:
389
raise NoSuchFile(path)
391
def final_kind(self, trans_id):
392
"""Determine the final file kind, after any changes applied.
394
Raises NoSuchFile if the file does not exist/has no contents.
395
(It is conceivable that a path would be created without the
396
corresponding contents insertion command)
398
if trans_id in self._new_contents:
399
return self._new_contents[trans_id]
400
elif trans_id in self._removed_contents:
401
raise NoSuchFile(None)
403
return self.tree_kind(trans_id)
405
def tree_file_id(self, trans_id):
406
"""Determine the file id associated with the trans_id in the tree"""
408
path = self._tree_id_paths[trans_id]
410
# the file is a new, unversioned file, or invalid trans_id
412
# the file is old; the old id is still valid
413
if self._new_root == trans_id:
414
return self._tree.inventory.root.file_id
415
return self._tree.inventory.path2id(path)
417
def final_file_id(self, trans_id):
418
"""Determine the file id after any changes are applied, or None.
420
None indicates that the file will not be versioned after changes are
424
# there is a new id for this file
425
assert self._new_id[trans_id] is not None
426
return self._new_id[trans_id]
428
if trans_id in self._removed_id:
430
return self.tree_file_id(trans_id)
432
def inactive_file_id(self, trans_id):
433
"""Return the inactive file_id associated with a transaction id.
434
That is, the one in the tree or in non_present_ids.
435
The file_id may actually be active, too.
437
file_id = self.tree_file_id(trans_id)
438
if file_id is not None:
440
for key, value in self._non_present_ids.iteritems():
441
if value == trans_id:
444
def final_parent(self, trans_id):
445
"""Determine the parent file_id, after any changes are applied.
447
ROOT_PARENT is returned for the tree root.
450
return self._new_parent[trans_id]
452
return self.get_tree_parent(trans_id)
454
def final_name(self, trans_id):
455
"""Determine the final filename, after all changes are applied."""
457
return self._new_name[trans_id]
459
return os.path.basename(self._tree_id_paths[trans_id])
462
"""Return a map of parent: children for known parents.
464
Only new paths and parents of tree files with assigned ids are used.
467
items = list(self._new_parent.iteritems())
468
items.extend((t, self.final_parent(t)) for t in
469
self._tree_id_paths.keys())
470
for trans_id, parent_id in items:
471
if parent_id not in by_parent:
472
by_parent[parent_id] = set()
473
by_parent[parent_id].add(trans_id)
476
def path_changed(self, trans_id):
477
"""Return True if a trans_id's path has changed."""
478
return trans_id in self._new_name or trans_id in self._new_parent
480
def find_conflicts(self):
481
"""Find any violations of inventory or filesystem invariants"""
482
if self.__done is True:
483
raise ReusingTransform()
485
# ensure all children of all existent parents are known
486
# all children of non-existent parents are known, by definition.
487
self._add_tree_children()
488
by_parent = self.by_parent()
489
conflicts.extend(self._unversioned_parents(by_parent))
490
conflicts.extend(self._parent_loops())
491
conflicts.extend(self._duplicate_entries(by_parent))
492
conflicts.extend(self._duplicate_ids())
493
conflicts.extend(self._parent_type_conflicts(by_parent))
494
conflicts.extend(self._improper_versioning())
495
conflicts.extend(self._executability_conflicts())
496
conflicts.extend(self._overwrite_conflicts())
499
def _add_tree_children(self):
500
"""Add all the children of all active parents to the known paths.
502
Active parents are those which gain children, and those which are
503
removed. This is a necessary first step in detecting conflicts.
505
parents = self.by_parent().keys()
506
parents.extend([t for t in self._removed_contents if
507
self.tree_kind(t) == 'directory'])
508
for trans_id in self._removed_id:
509
file_id = self.tree_file_id(trans_id)
510
if self._tree.inventory[file_id].kind == 'directory':
511
parents.append(trans_id)
513
for parent_id in parents:
514
# ensure that all children are registered with the transaction
515
list(self.iter_tree_children(parent_id))
517
def iter_tree_children(self, parent_id):
518
"""Iterate through the entry's tree children, if any"""
520
path = self._tree_id_paths[parent_id]
524
children = os.listdir(self._tree.abspath(path))
526
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
530
for child in children:
531
childpath = joinpath(path, child)
532
if self._tree.is_control_filename(childpath):
534
yield self.trans_id_tree_path(childpath)
536
def has_named_child(self, by_parent, parent_id, name):
538
children = by_parent[parent_id]
541
for child in children:
542
if self.final_name(child) == name:
545
path = self._tree_id_paths[parent_id]
548
childpath = joinpath(path, name)
549
child_id = self._tree_path_ids.get(childpath)
551
return lexists(self._tree.abspath(childpath))
553
if self.final_parent(child_id) != parent_id:
555
if child_id in self._removed_contents:
556
# XXX What about dangling file-ids?
561
def _parent_loops(self):
562
"""No entry should be its own ancestor"""
564
for trans_id in self._new_parent:
567
while parent_id is not ROOT_PARENT:
569
parent_id = self.final_parent(parent_id)
570
if parent_id == trans_id:
571
conflicts.append(('parent loop', trans_id))
572
if parent_id in seen:
576
def _unversioned_parents(self, by_parent):
577
"""If parent directories are versioned, children must be versioned."""
579
for parent_id, children in by_parent.iteritems():
580
if parent_id is ROOT_PARENT:
582
if self.final_file_id(parent_id) is not None:
584
for child_id in children:
585
if self.final_file_id(child_id) is not None:
586
conflicts.append(('unversioned parent', parent_id))
590
def _improper_versioning(self):
591
"""Cannot version a file with no contents, or a bad type.
593
However, existing entries with no contents are okay.
596
for trans_id in self._new_id.iterkeys():
598
kind = self.final_kind(trans_id)
600
conflicts.append(('versioning no contents', trans_id))
602
if not InventoryEntry.versionable_kind(kind):
603
conflicts.append(('versioning bad kind', trans_id, kind))
606
def _executability_conflicts(self):
607
"""Check for bad executability changes.
609
Only versioned files may have their executability set, because
610
1. only versioned entries can have executability under windows
611
2. only files can be executable. (The execute bit on a directory
612
does not indicate searchability)
615
for trans_id in self._new_executability:
616
if self.final_file_id(trans_id) is None:
617
conflicts.append(('unversioned executability', trans_id))
620
non_file = self.final_kind(trans_id) != "file"
624
conflicts.append(('non-file executability', trans_id))
627
def _overwrite_conflicts(self):
628
"""Check for overwrites (not permitted on Win32)"""
630
for trans_id in self._new_contents:
632
self.tree_kind(trans_id)
635
if trans_id not in self._removed_contents:
636
conflicts.append(('overwrite', trans_id,
637
self.final_name(trans_id)))
640
def _duplicate_entries(self, by_parent):
641
"""No directory may have two entries with the same name."""
643
for children in by_parent.itervalues():
644
name_ids = [(self.final_name(t), t) for t in children]
648
for name, trans_id in name_ids:
649
if name == last_name:
650
conflicts.append(('duplicate', last_trans_id, trans_id,
653
kind = self.final_kind(trans_id)
656
file_id = self.final_file_id(trans_id)
657
if kind is not None or file_id is not None:
659
last_trans_id = trans_id
662
def _duplicate_ids(self):
663
"""Each inventory id may only be used once"""
665
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
667
active_tree_ids = set((f for f in self._tree.inventory if
668
f not in removed_tree_ids))
669
for trans_id, file_id in self._new_id.iteritems():
670
if file_id in active_tree_ids:
671
old_trans_id = self.trans_id_tree_file_id(file_id)
672
conflicts.append(('duplicate id', old_trans_id, trans_id))
675
def _parent_type_conflicts(self, by_parent):
676
"""parents must have directory 'contents'."""
678
for parent_id, children in by_parent.iteritems():
679
if parent_id is ROOT_PARENT:
681
if not self._any_contents(children):
683
for child in children:
685
self.final_kind(child)
689
kind = self.final_kind(parent_id)
693
conflicts.append(('missing parent', parent_id))
694
elif kind != "directory":
695
conflicts.append(('non-directory parent', parent_id))
698
def _any_contents(self, trans_ids):
699
"""Return true if any of the trans_ids, will have contents."""
700
for trans_id in trans_ids:
702
kind = self.final_kind(trans_id)
709
"""Apply all changes to the inventory and filesystem.
711
If filesystem or inventory conflicts are present, MalformedTransform
714
conflicts = self.find_conflicts()
715
if len(conflicts) != 0:
716
raise MalformedTransform(conflicts=conflicts)
718
inv = self._tree.inventory
719
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
721
child_pb.update('Apply phase', 0, 2)
722
self._apply_removals(inv, limbo_inv)
723
child_pb.update('Apply phase', 1, 2)
724
modified_paths = self._apply_insertions(inv, limbo_inv)
727
self._tree._write_inventory(inv)
730
return _TransformResults(modified_paths)
732
def _limbo_name(self, trans_id):
733
"""Generate the limbo name of a file"""
734
return pathjoin(self._limbodir, trans_id)
736
def _apply_removals(self, inv, limbo_inv):
737
"""Perform tree operations that remove directory/inventory names.
739
That is, delete files that are to be deleted, and put any files that
740
need renaming into limbo. This must be done in strict child-to-parent
743
tree_paths = list(self._tree_path_ids.iteritems())
744
tree_paths.sort(reverse=True)
745
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
747
for num, data in enumerate(tree_paths):
748
path, trans_id = data
749
child_pb.update('removing file', num, len(tree_paths))
750
full_path = self._tree.abspath(path)
751
if trans_id in self._removed_contents:
752
delete_any(full_path)
753
elif trans_id in self._new_name or trans_id in \
756
os.rename(full_path, self._limbo_name(trans_id))
758
if e.errno != errno.ENOENT:
760
if trans_id in self._removed_id:
761
if trans_id == self._new_root:
762
file_id = self._tree.inventory.root.file_id
764
file_id = self.tree_file_id(trans_id)
766
elif trans_id in self._new_name or trans_id in self._new_parent:
767
file_id = self.tree_file_id(trans_id)
768
if file_id is not None:
769
limbo_inv[trans_id] = inv[file_id]
774
def _apply_insertions(self, inv, limbo_inv):
775
"""Perform tree operations that insert directory/inventory names.
777
That is, create any files that need to be created, and restore from
778
limbo any files that needed renaming. This must be done in strict
779
parent-to-child order.
781
new_paths = self.new_paths()
783
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
785
for num, (path, trans_id) in enumerate(new_paths):
786
child_pb.update('adding file', num, len(new_paths))
788
kind = self._new_contents[trans_id]
790
kind = contents = None
791
if trans_id in self._new_contents or \
792
self.path_changed(trans_id):
793
full_path = self._tree.abspath(path)
795
os.rename(self._limbo_name(trans_id), full_path)
797
# We may be renaming a dangling inventory id
798
if e.errno != errno.ENOENT:
800
if trans_id in self._new_contents:
801
modified_paths.append(full_path)
802
del self._new_contents[trans_id]
804
if trans_id in self._new_id:
806
kind = file_kind(self._tree.abspath(path))
807
inv.add_path(path, kind, self._new_id[trans_id])
808
elif trans_id in self._new_name or trans_id in\
810
entry = limbo_inv.get(trans_id)
811
if entry is not None:
812
entry.name = self.final_name(trans_id)
813
parent_path = os.path.dirname(path)
815
self._tree.inventory.path2id(parent_path)
818
# requires files and inventory entries to be in place
819
if trans_id in self._new_executability:
820
self._set_executability(path, inv, trans_id)
823
return modified_paths
825
def _set_executability(self, path, inv, trans_id):
826
"""Set the executability of versioned files """
827
file_id = inv.path2id(path)
828
new_executability = self._new_executability[trans_id]
829
inv[file_id].executable = new_executability
830
if supports_executable():
831
abspath = self._tree.abspath(path)
832
current_mode = os.stat(abspath).st_mode
833
if new_executability:
836
to_mode = current_mode | (0100 & ~umask)
837
# Enable x-bit for others only if they can read it.
838
if current_mode & 0004:
839
to_mode |= 0001 & ~umask
840
if current_mode & 0040:
841
to_mode |= 0010 & ~umask
843
to_mode = current_mode & ~0111
844
os.chmod(abspath, to_mode)
846
def _new_entry(self, name, parent_id, file_id):
847
"""Helper function to create a new filesystem entry."""
848
trans_id = self.create_path(name, parent_id)
849
if file_id is not None:
850
self.version_file(file_id, trans_id)
853
def new_file(self, name, parent_id, contents, file_id=None,
855
"""Convenience method to create files.
857
name is the name of the file to create.
858
parent_id is the transaction id of the parent directory of the file.
859
contents is an iterator of bytestrings, which will be used to produce
861
:param file_id: The inventory ID of the file, if it is to be versioned.
862
:param executable: Only valid when a file_id has been supplied.
864
trans_id = self._new_entry(name, parent_id, file_id)
865
# TODO: rather than scheduling a set_executable call,
866
# have create_file create the file with the right mode.
867
self.create_file(contents, trans_id)
868
if executable is not None:
869
self.set_executability(executable, trans_id)
872
def new_directory(self, name, parent_id, file_id=None):
873
"""Convenience method to create directories.
875
name is the name of the directory to create.
876
parent_id is the transaction id of the parent directory of the
878
file_id is the inventory ID of the directory, if it is to be versioned.
880
trans_id = self._new_entry(name, parent_id, file_id)
881
self.create_directory(trans_id)
884
def new_symlink(self, name, parent_id, target, file_id=None):
885
"""Convenience method to create symbolic link.
887
name is the name of the symlink to create.
888
parent_id is the transaction id of the parent directory of the symlink.
889
target is a bytestring of the target of the symlink.
890
file_id is the inventory ID of the file, if it is to be versioned.
892
trans_id = self._new_entry(name, parent_id, file_id)
893
self.create_symlink(target, trans_id)
896
def joinpath(parent, child):
897
"""Join tree-relative paths, handling the tree root specially"""
898
if parent is None or parent == "":
901
return pathjoin(parent, child)
904
class FinalPaths(object):
905
"""Make path calculation cheap by memoizing paths.
907
The underlying tree must not be manipulated between calls, or else
908
the results will likely be incorrect.
910
def __init__(self, transform):
911
object.__init__(self)
912
self._known_paths = {}
913
self.transform = transform
915
def _determine_path(self, trans_id):
916
if trans_id == self.transform.root:
918
name = self.transform.final_name(trans_id)
919
parent_id = self.transform.final_parent(trans_id)
920
if parent_id == self.transform.root:
923
return pathjoin(self.get_path(parent_id), name)
925
def get_path(self, trans_id):
926
"""Find the final path associated with a trans_id"""
927
if trans_id not in self._known_paths:
928
self._known_paths[trans_id] = self._determine_path(trans_id)
929
return self._known_paths[trans_id]
931
def topology_sorted_ids(tree):
932
"""Determine the topological order of the ids in a tree"""
933
file_ids = list(tree)
934
file_ids.sort(key=tree.id2path)
937
def build_tree(tree, wt):
938
"""Create working tree for a branch, using a Transaction."""
940
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
941
pp = ProgressPhase("Build phase", 2, top_pb)
942
tt = TreeTransform(wt)
945
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
946
file_ids = topology_sorted_ids(tree)
947
pb = bzrlib.ui.ui_factory.nested_progress_bar()
949
for num, file_id in enumerate(file_ids):
950
pb.update("Building tree", num, len(file_ids))
951
entry = tree.inventory[file_id]
952
if entry.parent_id is None:
954
if entry.parent_id not in file_trans_id:
955
raise repr(entry.parent_id)
956
parent_id = file_trans_id[entry.parent_id]
957
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
967
def new_by_entry(tt, entry, parent_id, tree):
968
"""Create a new file according to its inventory entry"""
972
contents = tree.get_file(entry.file_id).readlines()
973
executable = tree.is_executable(entry.file_id)
974
return tt.new_file(name, parent_id, contents, entry.file_id,
976
elif kind == 'directory':
977
return tt.new_directory(name, parent_id, entry.file_id)
978
elif kind == 'symlink':
979
target = tree.get_symlink_target(entry.file_id)
980
return tt.new_symlink(name, parent_id, target, entry.file_id)
982
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
983
"""Create new file contents according to an inventory entry."""
984
if entry.kind == "file":
986
lines = tree.get_file(entry.file_id).readlines()
987
tt.create_file(lines, trans_id, mode_id=mode_id)
988
elif entry.kind == "symlink":
989
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
990
elif entry.kind == "directory":
991
tt.create_directory(trans_id)
993
def create_entry_executability(tt, entry, trans_id):
994
"""Set the executability of a trans_id according to an inventory entry"""
995
if entry.kind == "file":
996
tt.set_executability(entry.executable, trans_id)
999
def find_interesting(working_tree, target_tree, filenames):
1000
"""Find the ids corresponding to specified filenames."""
1001
trees = (working_tree, target_tree)
1002
return tree.find_ids_across_trees(filenames, trees)
1005
def change_entry(tt, file_id, working_tree, target_tree,
1006
trans_id_file_id, backups, trans_id, by_parent):
1007
"""Replace a file_id's contents with those from a target tree."""
1008
e_trans_id = trans_id_file_id(file_id)
1009
entry = target_tree.inventory[file_id]
1010
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1013
mode_id = e_trans_id
1016
tt.delete_contents(e_trans_id)
1018
parent_trans_id = trans_id_file_id(entry.parent_id)
1019
backup_name = get_backup_name(entry, by_parent,
1020
parent_trans_id, tt)
1021
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1022
tt.unversion_file(e_trans_id)
1023
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1024
tt.version_file(file_id, e_trans_id)
1025
trans_id[file_id] = e_trans_id
1026
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1027
create_entry_executability(tt, entry, e_trans_id)
1030
tt.set_executability(entry.executable, e_trans_id)
1031
if tt.final_name(e_trans_id) != entry.name:
1034
parent_id = tt.final_parent(e_trans_id)
1035
parent_file_id = tt.final_file_id(parent_id)
1036
if parent_file_id != entry.parent_id:
1041
parent_trans_id = trans_id_file_id(entry.parent_id)
1042
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1045
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1046
"""Produce a backup-style name that appears to be available"""
1050
yield "%s.~%d~" % (entry.name, counter)
1052
for name in name_gen():
1053
if not tt.has_named_child(by_parent, parent_trans_id, name):
1056
def _entry_changes(file_id, entry, working_tree):
1057
"""Determine in which ways the inventory entry has changed.
1059
Returns booleans: has_contents, content_mod, meta_mod
1060
has_contents means there are currently contents, but they differ
1061
contents_mod means contents need to be modified
1062
meta_mod means the metadata needs to be modified
1064
cur_entry = working_tree.inventory[file_id]
1066
working_kind = working_tree.kind(file_id)
1069
has_contents = False
1072
if has_contents is True:
1073
if entry.kind != working_kind:
1074
contents_mod, meta_mod = True, False
1076
cur_entry._read_tree_state(working_tree.id2path(file_id),
1078
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1079
cur_entry._forget_tree_state()
1080
return has_contents, contents_mod, meta_mod
1083
def revert(working_tree, target_tree, filenames, backups=False,
1084
pb=DummyProgress()):
1085
"""Revert a working tree's contents to those of a target tree."""
1086
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1087
def interesting(file_id):
1088
return interesting_ids is None or file_id in interesting_ids
1090
tt = TreeTransform(working_tree, pb)
1092
merge_modified = working_tree.merge_modified()
1094
def trans_id_file_id(file_id):
1096
return trans_id[file_id]
1098
return tt.trans_id_tree_file_id(file_id)
1100
pp = ProgressPhase("Revert phase", 4, pb)
1102
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1104
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1106
by_parent = tt.by_parent()
1107
for id_num, file_id in enumerate(sorted_interesting):
1108
child_pb.update("Reverting file", id_num+1,
1109
len(sorted_interesting))
1110
if file_id not in working_tree.inventory:
1111
entry = target_tree.inventory[file_id]
1112
parent_id = trans_id_file_id(entry.parent_id)
1113
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1114
trans_id[file_id] = e_trans_id
1116
backup_this = backups
1117
if file_id in merge_modified:
1119
del merge_modified[file_id]
1120
change_entry(tt, file_id, working_tree, target_tree,
1121
trans_id_file_id, backup_this, trans_id,
1126
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1127
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1129
for id_num, file_id in enumerate(wt_interesting):
1130
child_pb.update("New file check", id_num+1,
1131
len(sorted_interesting))
1132
if file_id not in target_tree:
1133
trans_id = tt.trans_id_tree_file_id(file_id)
1134
tt.unversion_file(trans_id)
1136
file_kind = working_tree.kind(file_id)
1139
if file_kind != 'file' and file_kind is not None:
1140
keep_contents = False
1141
delete_merge_modified = False
1143
if (file_id in merge_modified and
1144
merge_modified[file_id] ==
1145
working_tree.get_file_sha1(file_id)):
1146
keep_contents = False
1147
delete_merge_modified = True
1149
keep_contents = True
1150
delete_merge_modified = False
1151
if not keep_contents:
1152
tt.delete_contents(trans_id)
1153
if delete_merge_modified:
1154
del merge_modified[file_id]
1158
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1160
raw_conflicts = resolve_conflicts(tt, child_pb)
1163
conflicts = cook_conflicts(raw_conflicts, tt)
1164
for conflict in conflicts:
1168
working_tree.set_merge_modified({})
1175
def resolve_conflicts(tt, pb=DummyProgress()):
1176
"""Make many conflict-resolution attempts, but die if they fail"""
1177
new_conflicts = set()
1180
pb.update('Resolution pass', n+1, 10)
1181
conflicts = tt.find_conflicts()
1182
if len(conflicts) == 0:
1183
return new_conflicts
1184
new_conflicts.update(conflict_pass(tt, conflicts))
1185
raise MalformedTransform(conflicts=conflicts)
1190
def conflict_pass(tt, conflicts):
1191
"""Resolve some classes of conflicts."""
1192
new_conflicts = set()
1193
for c_type, conflict in ((c[0], c) for c in conflicts):
1194
if c_type == 'duplicate id':
1195
tt.unversion_file(conflict[1])
1196
new_conflicts.add((c_type, 'Unversioned existing file',
1197
conflict[1], conflict[2], ))
1198
elif c_type == 'duplicate':
1199
# files that were renamed take precedence
1200
new_name = tt.final_name(conflict[1])+'.moved'
1201
final_parent = tt.final_parent(conflict[1])
1202
if tt.path_changed(conflict[1]):
1203
tt.adjust_path(new_name, final_parent, conflict[2])
1204
new_conflicts.add((c_type, 'Moved existing file to',
1205
conflict[2], conflict[1]))
1207
tt.adjust_path(new_name, final_parent, conflict[1])
1208
new_conflicts.add((c_type, 'Moved existing file to',
1209
conflict[1], conflict[2]))
1210
elif c_type == 'parent loop':
1211
# break the loop by undoing one of the ops that caused the loop
1213
while not tt.path_changed(cur):
1214
cur = tt.final_parent(cur)
1215
new_conflicts.add((c_type, 'Cancelled move', cur,
1216
tt.final_parent(cur),))
1217
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1219
elif c_type == 'missing parent':
1220
trans_id = conflict[1]
1222
tt.cancel_deletion(trans_id)
1223
new_conflicts.add((c_type, 'Not deleting', trans_id))
1225
tt.create_directory(trans_id)
1226
new_conflicts.add((c_type, 'Created directory.', trans_id))
1227
elif c_type == 'unversioned parent':
1228
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1229
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1230
return new_conflicts
1233
def cook_conflicts(raw_conflicts, tt):
1234
"""Generate a list of cooked conflicts, sorted by file path"""
1235
from bzrlib.conflicts import Conflict
1236
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1237
return sorted(conflict_iter, key=Conflict.sort_key)
1240
def iter_cook_conflicts(raw_conflicts, tt):
1241
from bzrlib.conflicts import Conflict
1243
for conflict in raw_conflicts:
1244
c_type = conflict[0]
1245
action = conflict[1]
1246
modified_path = fp.get_path(conflict[2])
1247
modified_id = tt.final_file_id(conflict[2])
1248
if len(conflict) == 3:
1249
yield Conflict.factory(c_type, action=action, path=modified_path,
1250
file_id=modified_id)
1253
conflicting_path = fp.get_path(conflict[3])
1254
conflicting_id = tt.final_file_id(conflict[3])
1255
yield Conflict.factory(c_type, action=action, path=modified_path,
1256
file_id=modified_id,
1257
conflict_path=conflicting_path,
1258
conflict_file_id=conflicting_id)