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
name = self._limbo_name(trans_id)
266
unique_add(self._new_contents, trans_id, 'file')
268
# Clean up the file, it never got registered so
269
# TreeTransform.finalize() won't clean it up.
274
for segment in contents:
278
self._set_mode(trans_id, mode_id, S_ISREG)
280
def _set_mode(self, trans_id, mode_id, typefunc):
281
"""Set the mode of new file contents.
282
The mode_id is the existing file to get the mode from (often the same
283
as trans_id). The operation is only performed if there's a mode match
284
according to typefunc.
289
old_path = self._tree_id_paths[mode_id]
293
mode = os.stat(old_path).st_mode
295
if e.errno == errno.ENOENT:
300
os.chmod(self._limbo_name(trans_id), mode)
302
def create_directory(self, trans_id):
303
"""Schedule creation of a new directory.
305
See also new_directory.
307
os.mkdir(self._limbo_name(trans_id))
308
unique_add(self._new_contents, trans_id, 'directory')
310
def create_symlink(self, target, trans_id):
311
"""Schedule creation of a new symbolic link.
313
target is a bytestring.
314
See also new_symlink.
316
os.symlink(target, self._limbo_name(trans_id))
317
unique_add(self._new_contents, trans_id, 'symlink')
319
def cancel_creation(self, trans_id):
320
"""Cancel the creation of new file contents."""
321
del self._new_contents[trans_id]
322
delete_any(self._limbo_name(trans_id))
324
def delete_contents(self, trans_id):
325
"""Schedule the contents of a path entry for deletion"""
326
self.tree_kind(trans_id)
327
self._removed_contents.add(trans_id)
329
def cancel_deletion(self, trans_id):
330
"""Cancel a scheduled deletion"""
331
self._removed_contents.remove(trans_id)
333
def unversion_file(self, trans_id):
334
"""Schedule a path entry to become unversioned"""
335
self._removed_id.add(trans_id)
337
def delete_versioned(self, trans_id):
338
"""Delete and unversion a versioned file"""
339
self.delete_contents(trans_id)
340
self.unversion_file(trans_id)
342
def set_executability(self, executability, trans_id):
343
"""Schedule setting of the 'execute' bit
344
To unschedule, set to None
346
if executability is None:
347
del self._new_executability[trans_id]
349
unique_add(self._new_executability, trans_id, executability)
351
def version_file(self, file_id, trans_id):
352
"""Schedule a file to become versioned."""
353
assert file_id is not None
354
unique_add(self._new_id, trans_id, file_id)
355
unique_add(self._r_new_id, file_id, trans_id)
357
def cancel_versioning(self, trans_id):
358
"""Undo a previous versioning of a file"""
359
file_id = self._new_id[trans_id]
360
del self._new_id[trans_id]
361
del self._r_new_id[file_id]
364
"""Determine the paths of all new and changed files"""
366
fp = FinalPaths(self)
367
for id_set in (self._new_name, self._new_parent, self._new_contents,
368
self._new_id, self._new_executability):
369
new_ids.update(id_set)
370
new_paths = [(fp.get_path(t), t) for t in new_ids]
374
def tree_kind(self, trans_id):
375
"""Determine the file kind in the working tree.
377
Raises NoSuchFile if the file does not exist
379
path = self._tree_id_paths.get(trans_id)
381
raise NoSuchFile(None)
383
return file_kind(self._tree.abspath(path))
385
if e.errno != errno.ENOENT:
388
raise NoSuchFile(path)
390
def final_kind(self, trans_id):
391
"""Determine the final file kind, after any changes applied.
393
Raises NoSuchFile if the file does not exist/has no contents.
394
(It is conceivable that a path would be created without the
395
corresponding contents insertion command)
397
if trans_id in self._new_contents:
398
return self._new_contents[trans_id]
399
elif trans_id in self._removed_contents:
400
raise NoSuchFile(None)
402
return self.tree_kind(trans_id)
404
def tree_file_id(self, trans_id):
405
"""Determine the file id associated with the trans_id in the tree"""
407
path = self._tree_id_paths[trans_id]
409
# the file is a new, unversioned file, or invalid trans_id
411
# the file is old; the old id is still valid
412
if self._new_root == trans_id:
413
return self._tree.inventory.root.file_id
414
return self._tree.inventory.path2id(path)
416
def final_file_id(self, trans_id):
417
"""Determine the file id after any changes are applied, or None.
419
None indicates that the file will not be versioned after changes are
423
# there is a new id for this file
424
assert self._new_id[trans_id] is not None
425
return self._new_id[trans_id]
427
if trans_id in self._removed_id:
429
return self.tree_file_id(trans_id)
431
def inactive_file_id(self, trans_id):
432
"""Return the inactive file_id associated with a transaction id.
433
That is, the one in the tree or in non_present_ids.
434
The file_id may actually be active, too.
436
file_id = self.tree_file_id(trans_id)
437
if file_id is not None:
439
for key, value in self._non_present_ids.iteritems():
440
if value == trans_id:
443
def final_parent(self, trans_id):
444
"""Determine the parent file_id, after any changes are applied.
446
ROOT_PARENT is returned for the tree root.
449
return self._new_parent[trans_id]
451
return self.get_tree_parent(trans_id)
453
def final_name(self, trans_id):
454
"""Determine the final filename, after all changes are applied."""
456
return self._new_name[trans_id]
458
return os.path.basename(self._tree_id_paths[trans_id])
461
"""Return a map of parent: children for known parents.
463
Only new paths and parents of tree files with assigned ids are used.
466
items = list(self._new_parent.iteritems())
467
items.extend((t, self.final_parent(t)) for t in
468
self._tree_id_paths.keys())
469
for trans_id, parent_id in items:
470
if parent_id not in by_parent:
471
by_parent[parent_id] = set()
472
by_parent[parent_id].add(trans_id)
475
def path_changed(self, trans_id):
476
"""Return True if a trans_id's path has changed."""
477
return trans_id in self._new_name or trans_id in self._new_parent
479
def find_conflicts(self):
480
"""Find any violations of inventory or filesystem invariants"""
481
if self.__done is True:
482
raise ReusingTransform()
484
# ensure all children of all existent parents are known
485
# all children of non-existent parents are known, by definition.
486
self._add_tree_children()
487
by_parent = self.by_parent()
488
conflicts.extend(self._unversioned_parents(by_parent))
489
conflicts.extend(self._parent_loops())
490
conflicts.extend(self._duplicate_entries(by_parent))
491
conflicts.extend(self._duplicate_ids())
492
conflicts.extend(self._parent_type_conflicts(by_parent))
493
conflicts.extend(self._improper_versioning())
494
conflicts.extend(self._executability_conflicts())
495
conflicts.extend(self._overwrite_conflicts())
498
def _add_tree_children(self):
499
"""Add all the children of all active parents to the known paths.
501
Active parents are those which gain children, and those which are
502
removed. This is a necessary first step in detecting conflicts.
504
parents = self.by_parent().keys()
505
parents.extend([t for t in self._removed_contents if
506
self.tree_kind(t) == 'directory'])
507
for trans_id in self._removed_id:
508
file_id = self.tree_file_id(trans_id)
509
if self._tree.inventory[file_id].kind in ('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)
1003
interesting_ids = None
1005
interesting_ids = set()
1006
for tree_path in filenames:
1009
file_id = tree.inventory.path2id(tree_path)
1010
if file_id is not None:
1011
interesting_ids.add(file_id)
1014
raise NotVersionedError(path=tree_path)
1016
pending = interesting_ids
1017
# now handle children of interesting ids
1018
# we loop so that we handle all children of each id in both trees
1019
while len(pending) > 0:
1021
for file_id in pending:
1023
if file_id not in tree:
1025
entry = tree.inventory[file_id]
1026
for child in getattr(entry, 'children', {}).itervalues():
1027
if child.file_id not in interesting_ids:
1028
new_pending.add(child.file_id)
1029
interesting_ids.update(new_pending)
1030
pending = new_pending
1031
return interesting_ids
1034
def change_entry(tt, file_id, working_tree, target_tree,
1035
trans_id_file_id, backups, trans_id, by_parent):
1036
"""Replace a file_id's contents with those from a target tree."""
1037
e_trans_id = trans_id_file_id(file_id)
1038
entry = target_tree.inventory[file_id]
1039
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1042
mode_id = e_trans_id
1045
tt.delete_contents(e_trans_id)
1047
parent_trans_id = trans_id_file_id(entry.parent_id)
1048
backup_name = get_backup_name(entry, by_parent,
1049
parent_trans_id, tt)
1050
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1051
tt.unversion_file(e_trans_id)
1052
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1053
tt.version_file(file_id, e_trans_id)
1054
trans_id[file_id] = e_trans_id
1055
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1056
create_entry_executability(tt, entry, e_trans_id)
1059
tt.set_executability(entry.executable, e_trans_id)
1060
if tt.final_name(e_trans_id) != entry.name:
1063
parent_id = tt.final_parent(e_trans_id)
1064
parent_file_id = tt.final_file_id(parent_id)
1065
if parent_file_id != entry.parent_id:
1070
parent_trans_id = trans_id_file_id(entry.parent_id)
1071
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1074
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1075
"""Produce a backup-style name that appears to be available"""
1079
yield "%s.~%d~" % (entry.name, counter)
1081
for name in name_gen():
1082
if not tt.has_named_child(by_parent, parent_trans_id, name):
1085
def _entry_changes(file_id, entry, working_tree):
1086
"""Determine in which ways the inventory entry has changed.
1088
Returns booleans: has_contents, content_mod, meta_mod
1089
has_contents means there are currently contents, but they differ
1090
contents_mod means contents need to be modified
1091
meta_mod means the metadata needs to be modified
1093
cur_entry = working_tree.inventory[file_id]
1095
working_kind = working_tree.kind(file_id)
1098
has_contents = False
1101
if has_contents is True:
1102
real_e_kind = entry.kind
1103
if real_e_kind == 'root_directory':
1104
real_e_kind = 'directory'
1105
if real_e_kind != working_kind:
1106
contents_mod, meta_mod = True, False
1108
cur_entry._read_tree_state(working_tree.id2path(file_id),
1110
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1111
cur_entry._forget_tree_state()
1112
return has_contents, contents_mod, meta_mod
1115
def revert(working_tree, target_tree, filenames, backups=False,
1116
pb=DummyProgress()):
1117
"""Revert a working tree's contents to those of a target tree."""
1118
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1119
def interesting(file_id):
1120
return interesting_ids is None or file_id in interesting_ids
1122
tt = TreeTransform(working_tree, pb)
1124
merge_modified = working_tree.merge_modified()
1126
def trans_id_file_id(file_id):
1128
return trans_id[file_id]
1130
return tt.trans_id_tree_file_id(file_id)
1132
pp = ProgressPhase("Revert phase", 4, pb)
1134
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1136
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1138
by_parent = tt.by_parent()
1139
for id_num, file_id in enumerate(sorted_interesting):
1140
child_pb.update("Reverting file", id_num+1,
1141
len(sorted_interesting))
1142
if file_id not in working_tree.inventory:
1143
entry = target_tree.inventory[file_id]
1144
parent_id = trans_id_file_id(entry.parent_id)
1145
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1146
trans_id[file_id] = e_trans_id
1148
backup_this = backups
1149
if file_id in merge_modified:
1151
del merge_modified[file_id]
1152
change_entry(tt, file_id, working_tree, target_tree,
1153
trans_id_file_id, backup_this, trans_id,
1158
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1159
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1161
for id_num, file_id in enumerate(wt_interesting):
1162
child_pb.update("New file check", id_num+1,
1163
len(sorted_interesting))
1164
if file_id not in target_tree:
1165
trans_id = tt.trans_id_tree_file_id(file_id)
1166
tt.unversion_file(trans_id)
1167
if file_id in merge_modified:
1168
tt.delete_contents(trans_id)
1169
del merge_modified[file_id]
1173
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1175
raw_conflicts = resolve_conflicts(tt, child_pb)
1178
conflicts = cook_conflicts(raw_conflicts, tt)
1179
for conflict in conflicts:
1183
working_tree.set_merge_modified({})
1190
def resolve_conflicts(tt, pb=DummyProgress()):
1191
"""Make many conflict-resolution attempts, but die if they fail"""
1192
new_conflicts = set()
1195
pb.update('Resolution pass', n+1, 10)
1196
conflicts = tt.find_conflicts()
1197
if len(conflicts) == 0:
1198
return new_conflicts
1199
new_conflicts.update(conflict_pass(tt, conflicts))
1200
raise MalformedTransform(conflicts=conflicts)
1205
def conflict_pass(tt, conflicts):
1206
"""Resolve some classes of conflicts."""
1207
new_conflicts = set()
1208
for c_type, conflict in ((c[0], c) for c in conflicts):
1209
if c_type == 'duplicate id':
1210
tt.unversion_file(conflict[1])
1211
new_conflicts.add((c_type, 'Unversioned existing file',
1212
conflict[1], conflict[2], ))
1213
elif c_type == 'duplicate':
1214
# files that were renamed take precedence
1215
new_name = tt.final_name(conflict[1])+'.moved'
1216
final_parent = tt.final_parent(conflict[1])
1217
if tt.path_changed(conflict[1]):
1218
tt.adjust_path(new_name, final_parent, conflict[2])
1219
new_conflicts.add((c_type, 'Moved existing file to',
1220
conflict[2], conflict[1]))
1222
tt.adjust_path(new_name, final_parent, conflict[1])
1223
new_conflicts.add((c_type, 'Moved existing file to',
1224
conflict[1], conflict[2]))
1225
elif c_type == 'parent loop':
1226
# break the loop by undoing one of the ops that caused the loop
1228
while not tt.path_changed(cur):
1229
cur = tt.final_parent(cur)
1230
new_conflicts.add((c_type, 'Cancelled move', cur,
1231
tt.final_parent(cur),))
1232
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1234
elif c_type == 'missing parent':
1235
trans_id = conflict[1]
1237
tt.cancel_deletion(trans_id)
1238
new_conflicts.add((c_type, 'Not deleting', trans_id))
1240
tt.create_directory(trans_id)
1241
new_conflicts.add((c_type, 'Created directory.', trans_id))
1242
elif c_type == 'unversioned parent':
1243
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1244
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1245
return new_conflicts
1248
def cook_conflicts(raw_conflicts, tt):
1249
"""Generate a list of cooked conflicts, sorted by file path"""
1250
from bzrlib.conflicts import Conflict
1251
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1252
return sorted(conflict_iter, key=Conflict.sort_key)
1255
def iter_cook_conflicts(raw_conflicts, tt):
1256
from bzrlib.conflicts import Conflict
1258
for conflict in raw_conflicts:
1259
c_type = conflict[0]
1260
action = conflict[1]
1261
modified_path = fp.get_path(conflict[2])
1262
modified_id = tt.final_file_id(conflict[2])
1263
if len(conflict) == 3:
1264
yield Conflict.factory(c_type, action=action, path=modified_path,
1265
file_id=modified_id)
1268
conflicting_path = fp.get_path(conflict[3])
1269
conflicting_id = tt.final_file_id(conflict[3])
1270
yield Conflict.factory(c_type, action=action, path=modified_path,
1271
file_id=modified_id,
1272
conflict_path=conflicting_path,
1273
conflict_file_id=conflicting_id)