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, NoFinalPath)
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]
460
return os.path.basename(self._tree_id_paths[trans_id])
462
raise NoFinalPath(trans_id, self)
465
"""Return a map of parent: children for known parents.
467
Only new paths and parents of tree files with assigned ids are used.
470
items = list(self._new_parent.iteritems())
471
items.extend((t, self.final_parent(t)) for t in
472
self._tree_id_paths.keys())
473
for trans_id, parent_id in items:
474
if parent_id not in by_parent:
475
by_parent[parent_id] = set()
476
by_parent[parent_id].add(trans_id)
479
def path_changed(self, trans_id):
480
"""Return True if a trans_id's path has changed."""
481
return trans_id in self._new_name or trans_id in self._new_parent
483
def find_conflicts(self):
484
"""Find any violations of inventory or filesystem invariants"""
485
if self.__done is True:
486
raise ReusingTransform()
488
# ensure all children of all existent parents are known
489
# all children of non-existent parents are known, by definition.
490
self._add_tree_children()
491
by_parent = self.by_parent()
492
conflicts.extend(self._unversioned_parents(by_parent))
493
conflicts.extend(self._parent_loops())
494
conflicts.extend(self._duplicate_entries(by_parent))
495
conflicts.extend(self._duplicate_ids())
496
conflicts.extend(self._parent_type_conflicts(by_parent))
497
conflicts.extend(self._improper_versioning())
498
conflicts.extend(self._executability_conflicts())
499
conflicts.extend(self._overwrite_conflicts())
502
def _add_tree_children(self):
503
"""Add all the children of all active parents to the known paths.
505
Active parents are those which gain children, and those which are
506
removed. This is a necessary first step in detecting conflicts.
508
parents = self.by_parent().keys()
509
parents.extend([t for t in self._removed_contents if
510
self.tree_kind(t) == 'directory'])
511
for trans_id in self._removed_id:
512
file_id = self.tree_file_id(trans_id)
513
if self._tree.inventory[file_id].kind == 'directory':
514
parents.append(trans_id)
516
for parent_id in parents:
517
# ensure that all children are registered with the transaction
518
list(self.iter_tree_children(parent_id))
520
def iter_tree_children(self, parent_id):
521
"""Iterate through the entry's tree children, if any"""
523
path = self._tree_id_paths[parent_id]
527
children = os.listdir(self._tree.abspath(path))
529
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
533
for child in children:
534
childpath = joinpath(path, child)
535
if self._tree.is_control_filename(childpath):
537
yield self.trans_id_tree_path(childpath)
539
def has_named_child(self, by_parent, parent_id, name):
541
children = by_parent[parent_id]
544
for child in children:
545
if self.final_name(child) == name:
548
path = self._tree_id_paths[parent_id]
551
childpath = joinpath(path, name)
552
child_id = self._tree_path_ids.get(childpath)
554
return lexists(self._tree.abspath(childpath))
556
if self.final_parent(child_id) != parent_id:
558
if child_id in self._removed_contents:
559
# XXX What about dangling file-ids?
564
def _parent_loops(self):
565
"""No entry should be its own ancestor"""
567
for trans_id in self._new_parent:
570
while parent_id is not ROOT_PARENT:
573
parent_id = self.final_parent(parent_id)
576
if parent_id == trans_id:
577
conflicts.append(('parent loop', trans_id))
578
if parent_id in seen:
582
def _unversioned_parents(self, by_parent):
583
"""If parent directories are versioned, children must be versioned."""
585
for parent_id, children in by_parent.iteritems():
586
if parent_id is ROOT_PARENT:
588
if self.final_file_id(parent_id) is not None:
590
for child_id in children:
591
if self.final_file_id(child_id) is not None:
592
conflicts.append(('unversioned parent', parent_id))
596
def _improper_versioning(self):
597
"""Cannot version a file with no contents, or a bad type.
599
However, existing entries with no contents are okay.
602
for trans_id in self._new_id.iterkeys():
604
kind = self.final_kind(trans_id)
606
conflicts.append(('versioning no contents', trans_id))
608
if not InventoryEntry.versionable_kind(kind):
609
conflicts.append(('versioning bad kind', trans_id, kind))
612
def _executability_conflicts(self):
613
"""Check for bad executability changes.
615
Only versioned files may have their executability set, because
616
1. only versioned entries can have executability under windows
617
2. only files can be executable. (The execute bit on a directory
618
does not indicate searchability)
621
for trans_id in self._new_executability:
622
if self.final_file_id(trans_id) is None:
623
conflicts.append(('unversioned executability', trans_id))
626
non_file = self.final_kind(trans_id) != "file"
630
conflicts.append(('non-file executability', trans_id))
633
def _overwrite_conflicts(self):
634
"""Check for overwrites (not permitted on Win32)"""
636
for trans_id in self._new_contents:
638
self.tree_kind(trans_id)
641
if trans_id not in self._removed_contents:
642
conflicts.append(('overwrite', trans_id,
643
self.final_name(trans_id)))
646
def _duplicate_entries(self, by_parent):
647
"""No directory may have two entries with the same name."""
649
for children in by_parent.itervalues():
650
name_ids = [(self.final_name(t), t) for t in children]
654
for name, trans_id in name_ids:
655
if name == last_name:
656
conflicts.append(('duplicate', last_trans_id, trans_id,
659
kind = self.final_kind(trans_id)
662
file_id = self.final_file_id(trans_id)
663
if kind is not None or file_id is not None:
665
last_trans_id = trans_id
668
def _duplicate_ids(self):
669
"""Each inventory id may only be used once"""
671
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
673
active_tree_ids = set((f for f in self._tree.inventory if
674
f not in removed_tree_ids))
675
for trans_id, file_id in self._new_id.iteritems():
676
if file_id in active_tree_ids:
677
old_trans_id = self.trans_id_tree_file_id(file_id)
678
conflicts.append(('duplicate id', old_trans_id, trans_id))
681
def _parent_type_conflicts(self, by_parent):
682
"""parents must have directory 'contents'."""
684
for parent_id, children in by_parent.iteritems():
685
if parent_id is ROOT_PARENT:
687
if not self._any_contents(children):
689
for child in children:
691
self.final_kind(child)
695
kind = self.final_kind(parent_id)
699
conflicts.append(('missing parent', parent_id))
700
elif kind != "directory":
701
conflicts.append(('non-directory parent', parent_id))
704
def _any_contents(self, trans_ids):
705
"""Return true if any of the trans_ids, will have contents."""
706
for trans_id in trans_ids:
708
kind = self.final_kind(trans_id)
715
"""Apply all changes to the inventory and filesystem.
717
If filesystem or inventory conflicts are present, MalformedTransform
720
conflicts = self.find_conflicts()
721
if len(conflicts) != 0:
722
raise MalformedTransform(conflicts=conflicts)
724
inv = self._tree.inventory
725
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
727
child_pb.update('Apply phase', 0, 2)
728
self._apply_removals(inv, limbo_inv)
729
child_pb.update('Apply phase', 1, 2)
730
modified_paths = self._apply_insertions(inv, limbo_inv)
733
self._tree._write_inventory(inv)
736
return _TransformResults(modified_paths)
738
def _limbo_name(self, trans_id):
739
"""Generate the limbo name of a file"""
740
return pathjoin(self._limbodir, trans_id)
742
def _apply_removals(self, inv, limbo_inv):
743
"""Perform tree operations that remove directory/inventory names.
745
That is, delete files that are to be deleted, and put any files that
746
need renaming into limbo. This must be done in strict child-to-parent
749
tree_paths = list(self._tree_path_ids.iteritems())
750
tree_paths.sort(reverse=True)
751
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
753
for num, data in enumerate(tree_paths):
754
path, trans_id = data
755
child_pb.update('removing file', num, len(tree_paths))
756
full_path = self._tree.abspath(path)
757
if trans_id in self._removed_contents:
758
delete_any(full_path)
759
elif trans_id in self._new_name or trans_id in \
762
os.rename(full_path, self._limbo_name(trans_id))
764
if e.errno != errno.ENOENT:
766
if trans_id in self._removed_id:
767
if trans_id == self._new_root:
768
file_id = self._tree.inventory.root.file_id
770
file_id = self.tree_file_id(trans_id)
772
elif trans_id in self._new_name or trans_id in self._new_parent:
773
file_id = self.tree_file_id(trans_id)
774
if file_id is not None:
775
limbo_inv[trans_id] = inv[file_id]
780
def _apply_insertions(self, inv, limbo_inv):
781
"""Perform tree operations that insert directory/inventory names.
783
That is, create any files that need to be created, and restore from
784
limbo any files that needed renaming. This must be done in strict
785
parent-to-child order.
787
new_paths = self.new_paths()
789
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
791
for num, (path, trans_id) in enumerate(new_paths):
792
child_pb.update('adding file', num, len(new_paths))
794
kind = self._new_contents[trans_id]
796
kind = contents = None
797
if trans_id in self._new_contents or \
798
self.path_changed(trans_id):
799
full_path = self._tree.abspath(path)
801
os.rename(self._limbo_name(trans_id), full_path)
803
# We may be renaming a dangling inventory id
804
if e.errno != errno.ENOENT:
806
if trans_id in self._new_contents:
807
modified_paths.append(full_path)
808
del self._new_contents[trans_id]
810
if trans_id in self._new_id:
812
kind = file_kind(self._tree.abspath(path))
813
inv.add_path(path, kind, self._new_id[trans_id])
814
elif trans_id in self._new_name or trans_id in\
816
entry = limbo_inv.get(trans_id)
817
if entry is not None:
818
entry.name = self.final_name(trans_id)
819
parent_path = os.path.dirname(path)
821
self._tree.inventory.path2id(parent_path)
824
# requires files and inventory entries to be in place
825
if trans_id in self._new_executability:
826
self._set_executability(path, inv, trans_id)
829
return modified_paths
831
def _set_executability(self, path, inv, trans_id):
832
"""Set the executability of versioned files """
833
file_id = inv.path2id(path)
834
new_executability = self._new_executability[trans_id]
835
inv[file_id].executable = new_executability
836
if supports_executable():
837
abspath = self._tree.abspath(path)
838
current_mode = os.stat(abspath).st_mode
839
if new_executability:
842
to_mode = current_mode | (0100 & ~umask)
843
# Enable x-bit for others only if they can read it.
844
if current_mode & 0004:
845
to_mode |= 0001 & ~umask
846
if current_mode & 0040:
847
to_mode |= 0010 & ~umask
849
to_mode = current_mode & ~0111
850
os.chmod(abspath, to_mode)
852
def _new_entry(self, name, parent_id, file_id):
853
"""Helper function to create a new filesystem entry."""
854
trans_id = self.create_path(name, parent_id)
855
if file_id is not None:
856
self.version_file(file_id, trans_id)
859
def new_file(self, name, parent_id, contents, file_id=None,
861
"""Convenience method to create files.
863
name is the name of the file to create.
864
parent_id is the transaction id of the parent directory of the file.
865
contents is an iterator of bytestrings, which will be used to produce
867
:param file_id: The inventory ID of the file, if it is to be versioned.
868
:param executable: Only valid when a file_id has been supplied.
870
trans_id = self._new_entry(name, parent_id, file_id)
871
# TODO: rather than scheduling a set_executable call,
872
# have create_file create the file with the right mode.
873
self.create_file(contents, trans_id)
874
if executable is not None:
875
self.set_executability(executable, trans_id)
878
def new_directory(self, name, parent_id, file_id=None):
879
"""Convenience method to create directories.
881
name is the name of the directory to create.
882
parent_id is the transaction id of the parent directory of the
884
file_id is the inventory ID of the directory, if it is to be versioned.
886
trans_id = self._new_entry(name, parent_id, file_id)
887
self.create_directory(trans_id)
890
def new_symlink(self, name, parent_id, target, file_id=None):
891
"""Convenience method to create symbolic link.
893
name is the name of the symlink to create.
894
parent_id is the transaction id of the parent directory of the symlink.
895
target is a bytestring of the target of the symlink.
896
file_id is the inventory ID of the file, if it is to be versioned.
898
trans_id = self._new_entry(name, parent_id, file_id)
899
self.create_symlink(target, trans_id)
902
def joinpath(parent, child):
903
"""Join tree-relative paths, handling the tree root specially"""
904
if parent is None or parent == "":
907
return pathjoin(parent, child)
910
class FinalPaths(object):
911
"""Make path calculation cheap by memoizing paths.
913
The underlying tree must not be manipulated between calls, or else
914
the results will likely be incorrect.
916
def __init__(self, transform):
917
object.__init__(self)
918
self._known_paths = {}
919
self.transform = transform
921
def _determine_path(self, trans_id):
922
if trans_id == self.transform.root:
924
name = self.transform.final_name(trans_id)
925
parent_id = self.transform.final_parent(trans_id)
926
if parent_id == self.transform.root:
929
return pathjoin(self.get_path(parent_id), name)
931
def get_path(self, trans_id):
932
"""Find the final path associated with a trans_id"""
933
if trans_id not in self._known_paths:
934
self._known_paths[trans_id] = self._determine_path(trans_id)
935
return self._known_paths[trans_id]
937
def topology_sorted_ids(tree):
938
"""Determine the topological order of the ids in a tree"""
939
file_ids = list(tree)
940
file_ids.sort(key=tree.id2path)
943
def build_tree(tree, wt):
944
"""Create working tree for a branch, using a Transaction."""
946
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
947
pp = ProgressPhase("Build phase", 2, top_pb)
948
if tree.inventory.root is not None:
949
wt.set_root_id(tree.inventory.root.file_id)
950
tt = TreeTransform(wt)
953
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
954
file_ids = topology_sorted_ids(tree)
955
pb = bzrlib.ui.ui_factory.nested_progress_bar()
957
for num, file_id in enumerate(file_ids):
958
pb.update("Building tree", num, len(file_ids))
959
entry = tree.inventory[file_id]
960
if entry.parent_id is None:
962
if entry.parent_id not in file_trans_id:
963
raise repr(entry.parent_id)
964
parent_id = file_trans_id[entry.parent_id]
965
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
975
def new_by_entry(tt, entry, parent_id, tree):
976
"""Create a new file according to its inventory entry"""
980
contents = tree.get_file(entry.file_id).readlines()
981
executable = tree.is_executable(entry.file_id)
982
return tt.new_file(name, parent_id, contents, entry.file_id,
984
elif kind == 'directory':
985
return tt.new_directory(name, parent_id, entry.file_id)
986
elif kind == 'symlink':
987
target = tree.get_symlink_target(entry.file_id)
988
return tt.new_symlink(name, parent_id, target, entry.file_id)
990
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
991
"""Create new file contents according to an inventory entry."""
992
if entry.kind == "file":
994
lines = tree.get_file(entry.file_id).readlines()
995
tt.create_file(lines, trans_id, mode_id=mode_id)
996
elif entry.kind == "symlink":
997
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
998
elif entry.kind == "directory":
999
tt.create_directory(trans_id)
1001
def create_entry_executability(tt, entry, trans_id):
1002
"""Set the executability of a trans_id according to an inventory entry"""
1003
if entry.kind == "file":
1004
tt.set_executability(entry.executable, trans_id)
1007
def find_interesting(working_tree, target_tree, filenames):
1008
"""Find the ids corresponding to specified filenames."""
1009
trees = (working_tree, target_tree)
1010
return tree.find_ids_across_trees(filenames, trees)
1013
def change_entry(tt, file_id, working_tree, target_tree,
1014
trans_id_file_id, backups, trans_id, by_parent):
1015
"""Replace a file_id's contents with those from a target tree."""
1016
e_trans_id = trans_id_file_id(file_id)
1017
entry = target_tree.inventory[file_id]
1018
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1021
mode_id = e_trans_id
1024
tt.delete_contents(e_trans_id)
1026
parent_trans_id = trans_id_file_id(entry.parent_id)
1027
backup_name = get_backup_name(entry, by_parent,
1028
parent_trans_id, tt)
1029
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1030
tt.unversion_file(e_trans_id)
1031
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1032
tt.version_file(file_id, e_trans_id)
1033
trans_id[file_id] = e_trans_id
1034
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1035
create_entry_executability(tt, entry, e_trans_id)
1038
tt.set_executability(entry.executable, e_trans_id)
1039
if tt.final_name(e_trans_id) != entry.name:
1042
parent_id = tt.final_parent(e_trans_id)
1043
parent_file_id = tt.final_file_id(parent_id)
1044
if parent_file_id != entry.parent_id:
1049
parent_trans_id = trans_id_file_id(entry.parent_id)
1050
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1053
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1054
"""Produce a backup-style name that appears to be available"""
1058
yield "%s.~%d~" % (entry.name, counter)
1060
for name in name_gen():
1061
if not tt.has_named_child(by_parent, parent_trans_id, name):
1064
def _entry_changes(file_id, entry, working_tree):
1065
"""Determine in which ways the inventory entry has changed.
1067
Returns booleans: has_contents, content_mod, meta_mod
1068
has_contents means there are currently contents, but they differ
1069
contents_mod means contents need to be modified
1070
meta_mod means the metadata needs to be modified
1072
cur_entry = working_tree.inventory[file_id]
1074
working_kind = working_tree.kind(file_id)
1077
has_contents = False
1080
if has_contents is True:
1081
if entry.kind != working_kind:
1082
contents_mod, meta_mod = True, False
1084
cur_entry._read_tree_state(working_tree.id2path(file_id),
1086
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1087
cur_entry._forget_tree_state()
1088
return has_contents, contents_mod, meta_mod
1091
def revert(working_tree, target_tree, filenames, backups=False,
1092
pb=DummyProgress()):
1093
"""Revert a working tree's contents to those of a target tree."""
1094
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1095
def interesting(file_id):
1096
return interesting_ids is None or file_id in interesting_ids
1098
tt = TreeTransform(working_tree, pb)
1100
merge_modified = working_tree.merge_modified()
1102
def trans_id_file_id(file_id):
1104
return trans_id[file_id]
1106
return tt.trans_id_tree_file_id(file_id)
1108
pp = ProgressPhase("Revert phase", 4, pb)
1110
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1112
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1114
by_parent = tt.by_parent()
1115
for id_num, file_id in enumerate(sorted_interesting):
1116
child_pb.update("Reverting file", id_num+1,
1117
len(sorted_interesting))
1118
if file_id not in working_tree.inventory:
1119
entry = target_tree.inventory[file_id]
1120
parent_id = trans_id_file_id(entry.parent_id)
1121
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1122
trans_id[file_id] = e_trans_id
1124
backup_this = backups
1125
if file_id in merge_modified:
1127
del merge_modified[file_id]
1128
change_entry(tt, file_id, working_tree, target_tree,
1129
trans_id_file_id, backup_this, trans_id,
1134
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1135
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1137
for id_num, file_id in enumerate(wt_interesting):
1138
if (working_tree.inventory.is_root(file_id) and
1139
len(target_tree.inventory) == 0):
1141
child_pb.update("New file check", id_num+1,
1142
len(sorted_interesting))
1143
if file_id not in target_tree:
1144
trans_id = tt.trans_id_tree_file_id(file_id)
1145
tt.unversion_file(trans_id)
1146
if file_id in merge_modified:
1147
tt.delete_contents(trans_id)
1148
del merge_modified[file_id]
1152
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1154
raw_conflicts = resolve_conflicts(tt, child_pb)
1157
conflicts = cook_conflicts(raw_conflicts, tt)
1158
for conflict in conflicts:
1162
working_tree.set_merge_modified({})
1169
def resolve_conflicts(tt, pb=DummyProgress()):
1170
"""Make many conflict-resolution attempts, but die if they fail"""
1171
new_conflicts = set()
1174
pb.update('Resolution pass', n+1, 10)
1175
conflicts = tt.find_conflicts()
1176
if len(conflicts) == 0:
1177
return new_conflicts
1178
new_conflicts.update(conflict_pass(tt, conflicts))
1179
raise MalformedTransform(conflicts=conflicts)
1184
def conflict_pass(tt, conflicts):
1185
"""Resolve some classes of conflicts."""
1186
new_conflicts = set()
1187
for c_type, conflict in ((c[0], c) for c in conflicts):
1188
if c_type == 'duplicate id':
1189
tt.unversion_file(conflict[1])
1190
new_conflicts.add((c_type, 'Unversioned existing file',
1191
conflict[1], conflict[2], ))
1192
elif c_type == 'duplicate':
1193
# files that were renamed take precedence
1194
new_name = tt.final_name(conflict[1])+'.moved'
1195
final_parent = tt.final_parent(conflict[1])
1196
if tt.path_changed(conflict[1]):
1197
tt.adjust_path(new_name, final_parent, conflict[2])
1198
new_conflicts.add((c_type, 'Moved existing file to',
1199
conflict[2], conflict[1]))
1201
tt.adjust_path(new_name, final_parent, conflict[1])
1202
new_conflicts.add((c_type, 'Moved existing file to',
1203
conflict[1], conflict[2]))
1204
elif c_type == 'parent loop':
1205
# break the loop by undoing one of the ops that caused the loop
1207
while not tt.path_changed(cur):
1208
cur = tt.final_parent(cur)
1209
new_conflicts.add((c_type, 'Cancelled move', cur,
1210
tt.final_parent(cur),))
1211
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1213
elif c_type == 'missing parent':
1214
trans_id = conflict[1]
1216
tt.cancel_deletion(trans_id)
1217
new_conflicts.add((c_type, 'Not deleting', trans_id))
1219
tt.create_directory(trans_id)
1220
new_conflicts.add((c_type, 'Created directory.', trans_id))
1221
elif c_type == 'unversioned parent':
1222
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1223
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1224
return new_conflicts
1227
def cook_conflicts(raw_conflicts, tt):
1228
"""Generate a list of cooked conflicts, sorted by file path"""
1229
from bzrlib.conflicts import Conflict
1230
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1231
return sorted(conflict_iter, key=Conflict.sort_key)
1234
def iter_cook_conflicts(raw_conflicts, tt):
1235
from bzrlib.conflicts import Conflict
1237
for conflict in raw_conflicts:
1238
c_type = conflict[0]
1239
action = conflict[1]
1240
modified_path = fp.get_path(conflict[2])
1241
modified_id = tt.final_file_id(conflict[2])
1242
if len(conflict) == 3:
1243
yield Conflict.factory(c_type, action=action, path=modified_path,
1244
file_id=modified_id)
1247
conflicting_path = fp.get_path(conflict[3])
1248
conflicting_id = tt.final_file_id(conflict[3])
1249
yield Conflict.factory(c_type, action=action, path=modified_path,
1250
file_id=modified_id,
1251
conflict_path=conflicting_path,
1252
conflict_file_id=conflicting_id)