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
30
import bzrlib.urlutils as urlutils
33
ROOT_PARENT = "root-parent"
36
def unique_add(map, key, value):
38
raise DuplicateKey(key=key)
42
class _TransformResults(object):
43
def __init__(self, modified_paths):
45
self.modified_paths = modified_paths
48
class TreeTransform(object):
49
"""Represent a tree transformation.
51
This object is designed to support incremental generation of the transform,
54
It is easy to produce malformed transforms, but they are generally
55
harmless. Attempting to apply a malformed transform will cause an
56
exception to be raised before any modifications are made to the tree.
58
Many kinds of malformed transforms can be corrected with the
59
resolve_conflicts function. The remaining ones indicate programming error,
60
such as trying to create a file with no path.
62
Two sets of file creation methods are supplied. Convenience methods are:
67
These are composed of the low-level methods:
69
* create_file or create_directory or create_symlink
73
def __init__(self, tree, pb=DummyProgress()):
74
"""Note: a write lock is taken on the tree.
76
Use TreeTransform.finalize() to release the lock
80
self._tree.lock_write()
82
control_files = self._tree._control_files
83
self._limbodir = urlutils.local_path_from_url(
84
control_files.controlfilename('limbo'))
86
os.mkdir(self._limbodir)
88
if e.errno == errno.EEXIST:
89
raise ExistingLimbo(self._limbodir)
97
self._new_contents = {}
98
self._removed_contents = set()
99
self._new_executability = {}
101
self._non_present_ids = {}
103
self._removed_id = set()
104
self._tree_path_ids = {}
105
self._tree_id_paths = {}
107
# Cache of realpath results, to speed up canonical_path
109
# Cache of relpath results, to speed up canonical_path
110
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
114
def __get_root(self):
115
return self._new_root
117
root = property(__get_root)
120
"""Release the working tree lock, if held, clean up limbo dir."""
121
if self._tree is None:
124
for trans_id, kind in self._new_contents.iteritems():
125
path = self._limbo_name(trans_id)
126
if kind == "directory":
131
os.rmdir(self._limbodir)
133
# We don't especially care *why* the dir is immortal.
134
raise ImmortalLimbo(self._limbodir)
139
def _assign_id(self):
140
"""Produce a new tranform id"""
141
new_id = "new-%s" % self._id_number
145
def create_path(self, name, parent):
146
"""Assign a transaction id to a new path"""
147
trans_id = self._assign_id()
148
unique_add(self._new_name, trans_id, name)
149
unique_add(self._new_parent, trans_id, parent)
152
def adjust_path(self, name, parent, trans_id):
153
"""Change the path that is assigned to a transaction id."""
154
if trans_id == self._new_root:
156
self._new_name[trans_id] = name
157
self._new_parent[trans_id] = parent
159
def adjust_root_path(self, name, parent):
160
"""Emulate moving the root by moving all children, instead.
162
We do this by undoing the association of root's transaction id with the
163
current tree. This allows us to create a new directory with that
164
transaction id. We unversion the root directory and version the
165
physically new directory, and hope someone versions the tree root
168
old_root = self._new_root
169
old_root_file_id = self.final_file_id(old_root)
170
# force moving all children of root
171
for child_id in self.iter_tree_children(old_root):
172
if child_id != parent:
173
self.adjust_path(self.final_name(child_id),
174
self.final_parent(child_id), child_id)
175
file_id = self.final_file_id(child_id)
176
if file_id is not None:
177
self.unversion_file(child_id)
178
self.version_file(file_id, child_id)
180
# the physical root needs a new transaction id
181
self._tree_path_ids.pop("")
182
self._tree_id_paths.pop(old_root)
183
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
184
if parent == old_root:
185
parent = self._new_root
186
self.adjust_path(name, parent, old_root)
187
self.create_directory(old_root)
188
self.version_file(old_root_file_id, old_root)
189
self.unversion_file(self._new_root)
191
def trans_id_tree_file_id(self, inventory_id):
192
"""Determine the transaction id of a working tree file.
194
This reflects only files that already exist, not ones that will be
195
added by transactions.
197
path = self._tree.inventory.id2path(inventory_id)
198
return self.trans_id_tree_path(path)
200
def trans_id_file_id(self, file_id):
201
"""Determine or set the transaction id associated with a file ID.
202
A new id is only created for file_ids that were never present. If
203
a transaction has been unversioned, it is deliberately still returned.
204
(this will likely lead to an unversioned parent conflict.)
206
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
207
return self._r_new_id[file_id]
208
elif file_id in self._tree.inventory:
209
return self.trans_id_tree_file_id(file_id)
210
elif file_id in self._non_present_ids:
211
return self._non_present_ids[file_id]
213
trans_id = self._assign_id()
214
self._non_present_ids[file_id] = trans_id
217
def canonical_path(self, path):
218
"""Get the canonical tree-relative path"""
219
# don't follow final symlinks
220
abs = self._tree.abspath(path)
221
if abs in self._relpaths:
222
return self._relpaths[abs]
223
dirname, basename = os.path.split(abs)
224
if dirname not in self._realpaths:
225
self._realpaths[dirname] = os.path.realpath(dirname)
226
dirname = self._realpaths[dirname]
227
abs = pathjoin(dirname, basename)
228
if dirname in self._relpaths:
229
relpath = pathjoin(self._relpaths[dirname], basename)
230
relpath = relpath.rstrip('/\\')
232
relpath = self._tree.relpath(abs)
233
self._relpaths[abs] = relpath
236
def trans_id_tree_path(self, path):
237
"""Determine (and maybe set) the transaction ID for a tree path."""
238
path = self.canonical_path(path)
239
if path not in self._tree_path_ids:
240
self._tree_path_ids[path] = self._assign_id()
241
self._tree_id_paths[self._tree_path_ids[path]] = path
242
return self._tree_path_ids[path]
244
def get_tree_parent(self, trans_id):
245
"""Determine id of the parent in the tree."""
246
path = self._tree_id_paths[trans_id]
249
return self.trans_id_tree_path(os.path.dirname(path))
251
def create_file(self, contents, trans_id, mode_id=None):
252
"""Schedule creation of a new file.
256
Contents is an iterator of strings, all of which will be written
257
to the target destination.
259
New file takes the permissions of any existing file with that id,
260
unless mode_id is specified.
262
f = file(self._limbo_name(trans_id), 'wb')
263
unique_add(self._new_contents, trans_id, 'file')
264
for segment in contents:
267
self._set_mode(trans_id, mode_id, S_ISREG)
269
def _set_mode(self, trans_id, mode_id, typefunc):
270
"""Set the mode of new file contents.
271
The mode_id is the existing file to get the mode from (often the same
272
as trans_id). The operation is only performed if there's a mode match
273
according to typefunc.
278
old_path = self._tree_id_paths[mode_id]
282
mode = os.stat(old_path).st_mode
284
if e.errno == errno.ENOENT:
289
os.chmod(self._limbo_name(trans_id), mode)
291
def create_directory(self, trans_id):
292
"""Schedule creation of a new directory.
294
See also new_directory.
296
os.mkdir(self._limbo_name(trans_id))
297
unique_add(self._new_contents, trans_id, 'directory')
299
def create_symlink(self, target, trans_id):
300
"""Schedule creation of a new symbolic link.
302
target is a bytestring.
303
See also new_symlink.
305
os.symlink(target, self._limbo_name(trans_id))
306
unique_add(self._new_contents, trans_id, 'symlink')
308
def cancel_creation(self, trans_id):
309
"""Cancel the creation of new file contents."""
310
del self._new_contents[trans_id]
311
delete_any(self._limbo_name(trans_id))
313
def delete_contents(self, trans_id):
314
"""Schedule the contents of a path entry for deletion"""
315
self.tree_kind(trans_id)
316
self._removed_contents.add(trans_id)
318
def cancel_deletion(self, trans_id):
319
"""Cancel a scheduled deletion"""
320
self._removed_contents.remove(trans_id)
322
def unversion_file(self, trans_id):
323
"""Schedule a path entry to become unversioned"""
324
self._removed_id.add(trans_id)
326
def delete_versioned(self, trans_id):
327
"""Delete and unversion a versioned file"""
328
self.delete_contents(trans_id)
329
self.unversion_file(trans_id)
331
def set_executability(self, executability, trans_id):
332
"""Schedule setting of the 'execute' bit
333
To unschedule, set to None
335
if executability is None:
336
del self._new_executability[trans_id]
338
unique_add(self._new_executability, trans_id, executability)
340
def version_file(self, file_id, trans_id):
341
"""Schedule a file to become versioned."""
342
assert file_id is not None
343
unique_add(self._new_id, trans_id, file_id)
344
unique_add(self._r_new_id, file_id, trans_id)
346
def cancel_versioning(self, trans_id):
347
"""Undo a previous versioning of a file"""
348
file_id = self._new_id[trans_id]
349
del self._new_id[trans_id]
350
del self._r_new_id[file_id]
353
"""Determine the paths of all new and changed files"""
355
fp = FinalPaths(self)
356
for id_set in (self._new_name, self._new_parent, self._new_contents,
357
self._new_id, self._new_executability):
358
new_ids.update(id_set)
359
new_paths = [(fp.get_path(t), t) for t in new_ids]
363
def tree_kind(self, trans_id):
364
"""Determine the file kind in the working tree.
366
Raises NoSuchFile if the file does not exist
368
path = self._tree_id_paths.get(trans_id)
370
raise NoSuchFile(None)
372
return file_kind(self._tree.abspath(path))
374
if e.errno != errno.ENOENT:
377
raise NoSuchFile(path)
379
def final_kind(self, trans_id):
380
"""Determine the final file kind, after any changes applied.
382
Raises NoSuchFile if the file does not exist/has no contents.
383
(It is conceivable that a path would be created without the
384
corresponding contents insertion command)
386
if trans_id in self._new_contents:
387
return self._new_contents[trans_id]
388
elif trans_id in self._removed_contents:
389
raise NoSuchFile(None)
391
return self.tree_kind(trans_id)
393
def tree_file_id(self, trans_id):
394
"""Determine the file id associated with the trans_id in the tree"""
396
path = self._tree_id_paths[trans_id]
398
# the file is a new, unversioned file, or invalid trans_id
400
# the file is old; the old id is still valid
401
if self._new_root == trans_id:
402
return self._tree.inventory.root.file_id
403
return self._tree.inventory.path2id(path)
405
def final_file_id(self, trans_id):
406
"""Determine the file id after any changes are applied, or None.
408
None indicates that the file will not be versioned after changes are
412
# there is a new id for this file
413
assert self._new_id[trans_id] is not None
414
return self._new_id[trans_id]
416
if trans_id in self._removed_id:
418
return self.tree_file_id(trans_id)
420
def inactive_file_id(self, trans_id):
421
"""Return the inactive file_id associated with a transaction id.
422
That is, the one in the tree or in non_present_ids.
423
The file_id may actually be active, too.
425
file_id = self.tree_file_id(trans_id)
426
if file_id is not None:
428
for key, value in self._non_present_ids.iteritems():
429
if value == trans_id:
432
def final_parent(self, trans_id):
433
"""Determine the parent file_id, after any changes are applied.
435
ROOT_PARENT is returned for the tree root.
438
return self._new_parent[trans_id]
440
return self.get_tree_parent(trans_id)
442
def final_name(self, trans_id):
443
"""Determine the final filename, after all changes are applied."""
445
return self._new_name[trans_id]
448
return os.path.basename(self._tree_id_paths[trans_id])
450
raise NoFinalPath(trans_id, self)
453
"""Return a map of parent: children for known parents.
455
Only new paths and parents of tree files with assigned ids are used.
458
items = list(self._new_parent.iteritems())
459
items.extend((t, self.final_parent(t)) for t in
460
self._tree_id_paths.keys())
461
for trans_id, parent_id in items:
462
if parent_id not in by_parent:
463
by_parent[parent_id] = set()
464
by_parent[parent_id].add(trans_id)
467
def path_changed(self, trans_id):
468
"""Return True if a trans_id's path has changed."""
469
return trans_id in self._new_name or trans_id in self._new_parent
471
def find_conflicts(self):
472
"""Find any violations of inventory or filesystem invariants"""
473
if self.__done is True:
474
raise ReusingTransform()
476
# ensure all children of all existent parents are known
477
# all children of non-existent parents are known, by definition.
478
self._add_tree_children()
479
by_parent = self.by_parent()
480
conflicts.extend(self._unversioned_parents(by_parent))
481
conflicts.extend(self._parent_loops())
482
conflicts.extend(self._duplicate_entries(by_parent))
483
conflicts.extend(self._duplicate_ids())
484
conflicts.extend(self._parent_type_conflicts(by_parent))
485
conflicts.extend(self._improper_versioning())
486
conflicts.extend(self._executability_conflicts())
487
conflicts.extend(self._overwrite_conflicts())
490
def _add_tree_children(self):
491
"""Add all the children of all active parents to the known paths.
493
Active parents are those which gain children, and those which are
494
removed. This is a necessary first step in detecting conflicts.
496
parents = self.by_parent().keys()
497
parents.extend([t for t in self._removed_contents if
498
self.tree_kind(t) == 'directory'])
499
for trans_id in self._removed_id:
500
file_id = self.tree_file_id(trans_id)
501
if self._tree.inventory[file_id].kind == 'directory':
502
parents.append(trans_id)
504
for parent_id in parents:
505
# ensure that all children are registered with the transaction
506
list(self.iter_tree_children(parent_id))
508
def iter_tree_children(self, parent_id):
509
"""Iterate through the entry's tree children, if any"""
511
path = self._tree_id_paths[parent_id]
515
children = os.listdir(self._tree.abspath(path))
517
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
521
for child in children:
522
childpath = joinpath(path, child)
523
if self._tree.is_control_filename(childpath):
525
yield self.trans_id_tree_path(childpath)
527
def has_named_child(self, by_parent, parent_id, name):
529
children = by_parent[parent_id]
532
for child in children:
533
if self.final_name(child) == name:
536
path = self._tree_id_paths[parent_id]
539
childpath = joinpath(path, name)
540
child_id = self._tree_path_ids.get(childpath)
542
return lexists(self._tree.abspath(childpath))
544
if tt.final_parent(child_id) != parent_id:
546
if child_id in tt._removed_contents:
547
# XXX What about dangling file-ids?
552
def _parent_loops(self):
553
"""No entry should be its own ancestor"""
555
for trans_id in self._new_parent:
558
while parent_id is not ROOT_PARENT:
561
parent_id = self.final_parent(parent_id)
564
if parent_id == trans_id:
565
conflicts.append(('parent loop', trans_id))
566
if parent_id in seen:
570
def _unversioned_parents(self, by_parent):
571
"""If parent directories are versioned, children must be versioned."""
573
for parent_id, children in by_parent.iteritems():
574
if parent_id is ROOT_PARENT:
576
if self.final_file_id(parent_id) is not None:
578
for child_id in children:
579
if self.final_file_id(child_id) is not None:
580
conflicts.append(('unversioned parent', parent_id))
584
def _improper_versioning(self):
585
"""Cannot version a file with no contents, or a bad type.
587
However, existing entries with no contents are okay.
590
for trans_id in self._new_id.iterkeys():
592
kind = self.final_kind(trans_id)
594
conflicts.append(('versioning no contents', trans_id))
596
if not InventoryEntry.versionable_kind(kind):
597
conflicts.append(('versioning bad kind', trans_id, kind))
600
def _executability_conflicts(self):
601
"""Check for bad executability changes.
603
Only versioned files may have their executability set, because
604
1. only versioned entries can have executability under windows
605
2. only files can be executable. (The execute bit on a directory
606
does not indicate searchability)
609
for trans_id in self._new_executability:
610
if self.final_file_id(trans_id) is None:
611
conflicts.append(('unversioned executability', trans_id))
614
non_file = self.final_kind(trans_id) != "file"
618
conflicts.append(('non-file executability', trans_id))
621
def _overwrite_conflicts(self):
622
"""Check for overwrites (not permitted on Win32)"""
624
for trans_id in self._new_contents:
626
self.tree_kind(trans_id)
629
if trans_id not in self._removed_contents:
630
conflicts.append(('overwrite', trans_id,
631
self.final_name(trans_id)))
634
def _duplicate_entries(self, by_parent):
635
"""No directory may have two entries with the same name."""
637
for children in by_parent.itervalues():
638
name_ids = [(self.final_name(t), t) for t in children]
642
for name, trans_id in name_ids:
643
if name == last_name:
644
conflicts.append(('duplicate', last_trans_id, trans_id,
647
kind = self.final_kind(trans_id)
650
file_id = self.final_file_id(trans_id)
651
if kind is not None or file_id is not None:
653
last_trans_id = trans_id
656
def _duplicate_ids(self):
657
"""Each inventory id may only be used once"""
659
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
661
active_tree_ids = set((f for f in self._tree.inventory if
662
f not in removed_tree_ids))
663
for trans_id, file_id in self._new_id.iteritems():
664
if file_id in active_tree_ids:
665
old_trans_id = self.trans_id_tree_file_id(file_id)
666
conflicts.append(('duplicate id', old_trans_id, trans_id))
669
def _parent_type_conflicts(self, by_parent):
670
"""parents must have directory 'contents'."""
672
for parent_id, children in by_parent.iteritems():
673
if parent_id is ROOT_PARENT:
675
if not self._any_contents(children):
677
for child in children:
679
self.final_kind(child)
683
kind = self.final_kind(parent_id)
687
conflicts.append(('missing parent', parent_id))
688
elif kind != "directory":
689
conflicts.append(('non-directory parent', parent_id))
692
def _any_contents(self, trans_ids):
693
"""Return true if any of the trans_ids, will have contents."""
694
for trans_id in trans_ids:
696
kind = self.final_kind(trans_id)
703
"""Apply all changes to the inventory and filesystem.
705
If filesystem or inventory conflicts are present, MalformedTransform
708
conflicts = self.find_conflicts()
709
if len(conflicts) != 0:
710
raise MalformedTransform(conflicts=conflicts)
712
inv = self._tree.inventory
713
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
715
child_pb.update('Apply phase', 0, 2)
716
self._apply_removals(inv, limbo_inv)
717
child_pb.update('Apply phase', 1, 2)
718
modified_paths = self._apply_insertions(inv, limbo_inv)
721
self._tree._write_inventory(inv)
724
return _TransformResults(modified_paths)
726
def _limbo_name(self, trans_id):
727
"""Generate the limbo name of a file"""
728
return pathjoin(self._limbodir, trans_id)
730
def _apply_removals(self, inv, limbo_inv):
731
"""Perform tree operations that remove directory/inventory names.
733
That is, delete files that are to be deleted, and put any files that
734
need renaming into limbo. This must be done in strict child-to-parent
737
tree_paths = list(self._tree_path_ids.iteritems())
738
tree_paths.sort(reverse=True)
739
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
741
for num, data in enumerate(tree_paths):
742
path, trans_id = data
743
child_pb.update('removing file', num, len(tree_paths))
744
full_path = self._tree.abspath(path)
745
if trans_id in self._removed_contents:
746
delete_any(full_path)
747
elif trans_id in self._new_name or trans_id in \
750
os.rename(full_path, self._limbo_name(trans_id))
752
if e.errno != errno.ENOENT:
754
if trans_id in self._removed_id:
755
if trans_id == self._new_root:
756
file_id = self._tree.inventory.root.file_id
758
file_id = self.tree_file_id(trans_id)
760
elif trans_id in self._new_name or trans_id in self._new_parent:
761
file_id = self.tree_file_id(trans_id)
762
if file_id is not None:
763
limbo_inv[trans_id] = inv[file_id]
768
def _apply_insertions(self, inv, limbo_inv):
769
"""Perform tree operations that insert directory/inventory names.
771
That is, create any files that need to be created, and restore from
772
limbo any files that needed renaming. This must be done in strict
773
parent-to-child order.
775
new_paths = self.new_paths()
777
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
779
for num, (path, trans_id) in enumerate(new_paths):
780
child_pb.update('adding file', num, len(new_paths))
782
kind = self._new_contents[trans_id]
784
kind = contents = None
785
if trans_id in self._new_contents or \
786
self.path_changed(trans_id):
787
full_path = self._tree.abspath(path)
789
os.rename(self._limbo_name(trans_id), full_path)
791
# We may be renaming a dangling inventory id
792
if e.errno != errno.ENOENT:
794
if trans_id in self._new_contents:
795
modified_paths.append(full_path)
796
del self._new_contents[trans_id]
798
if trans_id in self._new_id:
800
kind = file_kind(self._tree.abspath(path))
801
inv.add_path(path, kind, self._new_id[trans_id])
802
elif trans_id in self._new_name or trans_id in\
804
entry = limbo_inv.get(trans_id)
805
if entry is not None:
806
entry.name = self.final_name(trans_id)
807
parent_path = os.path.dirname(path)
809
self._tree.inventory.path2id(parent_path)
812
# requires files and inventory entries to be in place
813
if trans_id in self._new_executability:
814
self._set_executability(path, inv, trans_id)
817
return modified_paths
819
def _set_executability(self, path, inv, trans_id):
820
"""Set the executability of versioned files """
821
file_id = inv.path2id(path)
822
new_executability = self._new_executability[trans_id]
823
inv[file_id].executable = new_executability
824
if supports_executable():
825
abspath = self._tree.abspath(path)
826
current_mode = os.stat(abspath).st_mode
827
if new_executability:
830
to_mode = current_mode | (0100 & ~umask)
831
# Enable x-bit for others only if they can read it.
832
if current_mode & 0004:
833
to_mode |= 0001 & ~umask
834
if current_mode & 0040:
835
to_mode |= 0010 & ~umask
837
to_mode = current_mode & ~0111
838
os.chmod(abspath, to_mode)
840
def _new_entry(self, name, parent_id, file_id):
841
"""Helper function to create a new filesystem entry."""
842
trans_id = self.create_path(name, parent_id)
843
if file_id is not None:
844
self.version_file(file_id, trans_id)
847
def new_file(self, name, parent_id, contents, file_id=None,
849
"""Convenience method to create files.
851
name is the name of the file to create.
852
parent_id is the transaction id of the parent directory of the file.
853
contents is an iterator of bytestrings, which will be used to produce
855
:param file_id: The inventory ID of the file, if it is to be versioned.
856
:param executable: Only valid when a file_id has been supplied.
858
trans_id = self._new_entry(name, parent_id, file_id)
859
# TODO: rather than scheduling a set_executable call,
860
# have create_file create the file with the right mode.
861
self.create_file(contents, trans_id)
862
if executable is not None:
863
self.set_executability(executable, trans_id)
866
def new_directory(self, name, parent_id, file_id=None):
867
"""Convenience method to create directories.
869
name is the name of the directory to create.
870
parent_id is the transaction id of the parent directory of the
872
file_id is the inventory ID of the directory, if it is to be versioned.
874
trans_id = self._new_entry(name, parent_id, file_id)
875
self.create_directory(trans_id)
878
def new_symlink(self, name, parent_id, target, file_id=None):
879
"""Convenience method to create symbolic link.
881
name is the name of the symlink to create.
882
parent_id is the transaction id of the parent directory of the symlink.
883
target is a bytestring of the target of the symlink.
884
file_id is the inventory ID of the file, if it is to be versioned.
886
trans_id = self._new_entry(name, parent_id, file_id)
887
self.create_symlink(target, trans_id)
890
def joinpath(parent, child):
891
"""Join tree-relative paths, handling the tree root specially"""
892
if parent is None or parent == "":
895
return pathjoin(parent, child)
898
class FinalPaths(object):
899
"""Make path calculation cheap by memoizing paths.
901
The underlying tree must not be manipulated between calls, or else
902
the results will likely be incorrect.
904
def __init__(self, transform):
905
object.__init__(self)
906
self._known_paths = {}
907
self.transform = transform
909
def _determine_path(self, trans_id):
910
if trans_id == self.transform.root:
912
name = self.transform.final_name(trans_id)
913
parent_id = self.transform.final_parent(trans_id)
914
if parent_id == self.transform.root:
917
return pathjoin(self.get_path(parent_id), name)
919
def get_path(self, trans_id):
920
"""Find the final path associated with a trans_id"""
921
if trans_id not in self._known_paths:
922
self._known_paths[trans_id] = self._determine_path(trans_id)
923
return self._known_paths[trans_id]
925
def topology_sorted_ids(tree):
926
"""Determine the topological order of the ids in a tree"""
927
file_ids = list(tree)
928
file_ids.sort(key=tree.id2path)
931
def build_tree(tree, wt):
932
"""Create working tree for a branch, using a Transaction."""
934
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
935
pp = ProgressPhase("Build phase", 2, top_pb)
936
if tree.inventory.root is not None:
937
wt.set_root_id(tree.inventory.root.file_id)
938
tt = TreeTransform(wt)
941
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
942
file_ids = topology_sorted_ids(tree)
943
pb = bzrlib.ui.ui_factory.nested_progress_bar()
945
for num, file_id in enumerate(file_ids):
946
pb.update("Building tree", num, len(file_ids))
947
entry = tree.inventory[file_id]
948
if entry.parent_id is None:
950
if entry.parent_id not in file_trans_id:
951
raise repr(entry.parent_id)
952
parent_id = file_trans_id[entry.parent_id]
953
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
963
def new_by_entry(tt, entry, parent_id, tree):
964
"""Create a new file according to its inventory entry"""
968
contents = tree.get_file(entry.file_id).readlines()
969
executable = tree.is_executable(entry.file_id)
970
return tt.new_file(name, parent_id, contents, entry.file_id,
972
elif kind == 'directory':
973
return tt.new_directory(name, parent_id, entry.file_id)
974
elif kind == 'symlink':
975
target = tree.get_symlink_target(entry.file_id)
976
return tt.new_symlink(name, parent_id, target, entry.file_id)
978
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
979
"""Create new file contents according to an inventory entry."""
980
if entry.kind == "file":
982
lines = tree.get_file(entry.file_id).readlines()
983
tt.create_file(lines, trans_id, mode_id=mode_id)
984
elif entry.kind == "symlink":
985
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
986
elif entry.kind == "directory":
987
tt.create_directory(trans_id)
989
def create_entry_executability(tt, entry, trans_id):
990
"""Set the executability of a trans_id according to an inventory entry"""
991
if entry.kind == "file":
992
tt.set_executability(entry.executable, trans_id)
995
def find_interesting(working_tree, target_tree, filenames):
996
"""Find the ids corresponding to specified filenames."""
998
interesting_ids = None
1000
interesting_ids = set()
1001
for tree_path in filenames:
1003
for tree in (working_tree, target_tree):
1004
file_id = tree.inventory.path2id(tree_path)
1005
if file_id is not None:
1006
interesting_ids.add(file_id)
1009
raise NotVersionedError(path=tree_path)
1010
return interesting_ids
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
if e.errno != errno.ENOENT:
1079
has_contents = False
1082
if has_contents is True:
1083
if entry.kind != working_kind:
1084
contents_mod, meta_mod = True, False
1086
cur_entry._read_tree_state(working_tree.id2path(file_id),
1088
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1089
cur_entry._forget_tree_state()
1090
return has_contents, contents_mod, meta_mod
1093
def revert(working_tree, target_tree, filenames, backups=False,
1094
pb=DummyProgress()):
1095
"""Revert a working tree's contents to those of a target tree."""
1096
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1097
def interesting(file_id):
1098
return interesting_ids is None or file_id in interesting_ids
1100
tt = TreeTransform(working_tree, pb)
1102
merge_modified = working_tree.merge_modified()
1104
def trans_id_file_id(file_id):
1106
return trans_id[file_id]
1108
return tt.trans_id_tree_file_id(file_id)
1110
pp = ProgressPhase("Revert phase", 4, pb)
1112
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1114
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1116
by_parent = tt.by_parent()
1117
for id_num, file_id in enumerate(sorted_interesting):
1118
child_pb.update("Reverting file", id_num+1,
1119
len(sorted_interesting))
1120
if file_id not in working_tree.inventory:
1121
entry = target_tree.inventory[file_id]
1122
parent_id = trans_id_file_id(entry.parent_id)
1123
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1124
trans_id[file_id] = e_trans_id
1126
backup_this = backups
1127
if file_id in merge_modified:
1129
del merge_modified[file_id]
1130
change_entry(tt, file_id, working_tree, target_tree,
1131
trans_id_file_id, backup_this, trans_id,
1136
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1137
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1139
for id_num, file_id in enumerate(wt_interesting):
1140
if (working_tree.inventory.is_root(file_id) and
1141
len(target_tree.inventory) == 0):
1143
child_pb.update("New file check", id_num+1,
1144
len(sorted_interesting))
1145
if file_id not in target_tree:
1146
trans_id = tt.trans_id_tree_file_id(file_id)
1147
tt.unversion_file(trans_id)
1148
if file_id in merge_modified:
1149
tt.delete_contents(trans_id)
1150
del merge_modified[file_id]
1154
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1156
raw_conflicts = resolve_conflicts(tt, child_pb)
1159
conflicts = cook_conflicts(raw_conflicts, tt)
1160
for conflict in conflicts:
1164
working_tree.set_merge_modified({})
1171
def resolve_conflicts(tt, pb=DummyProgress()):
1172
"""Make many conflict-resolution attempts, but die if they fail"""
1173
new_conflicts = set()
1176
pb.update('Resolution pass', n+1, 10)
1177
conflicts = tt.find_conflicts()
1178
if len(conflicts) == 0:
1179
return new_conflicts
1180
new_conflicts.update(conflict_pass(tt, conflicts))
1181
raise MalformedTransform(conflicts=conflicts)
1186
def conflict_pass(tt, conflicts):
1187
"""Resolve some classes of conflicts."""
1188
new_conflicts = set()
1189
for c_type, conflict in ((c[0], c) for c in conflicts):
1190
if c_type == 'duplicate id':
1191
tt.unversion_file(conflict[1])
1192
new_conflicts.add((c_type, 'Unversioned existing file',
1193
conflict[1], conflict[2], ))
1194
elif c_type == 'duplicate':
1195
# files that were renamed take precedence
1196
new_name = tt.final_name(conflict[1])+'.moved'
1197
final_parent = tt.final_parent(conflict[1])
1198
if tt.path_changed(conflict[1]):
1199
tt.adjust_path(new_name, final_parent, conflict[2])
1200
new_conflicts.add((c_type, 'Moved existing file to',
1201
conflict[2], conflict[1]))
1203
tt.adjust_path(new_name, final_parent, conflict[1])
1204
new_conflicts.add((c_type, 'Moved existing file to',
1205
conflict[1], conflict[2]))
1206
elif c_type == 'parent loop':
1207
# break the loop by undoing one of the ops that caused the loop
1209
while not tt.path_changed(cur):
1210
cur = tt.final_parent(cur)
1211
new_conflicts.add((c_type, 'Cancelled move', cur,
1212
tt.final_parent(cur),))
1213
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1215
elif c_type == 'missing parent':
1216
trans_id = conflict[1]
1218
tt.cancel_deletion(trans_id)
1219
new_conflicts.add((c_type, 'Not deleting', trans_id))
1221
tt.create_directory(trans_id)
1222
new_conflicts.add((c_type, 'Created directory.', trans_id))
1223
elif c_type == 'unversioned parent':
1224
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1225
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1226
return new_conflicts
1229
def cook_conflicts(raw_conflicts, tt):
1230
"""Generate a list of cooked conflicts, sorted by file path"""
1231
from bzrlib.conflicts import Conflict
1232
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1233
return sorted(conflict_iter, key=Conflict.sort_key)
1236
def iter_cook_conflicts(raw_conflicts, tt):
1237
from bzrlib.conflicts import Conflict
1239
for conflict in raw_conflicts:
1240
c_type = conflict[0]
1241
action = conflict[1]
1242
modified_path = fp.get_path(conflict[2])
1243
modified_id = tt.final_file_id(conflict[2])
1244
if len(conflict) == 3:
1245
yield Conflict.factory(c_type, action=action, path=modified_path,
1246
file_id=modified_id)
1249
conflicting_path = fp.get_path(conflict[3])
1250
conflicting_id = tt.final_file_id(conflict[3])
1251
yield Conflict.factory(c_type, action=action, path=modified_path,
1252
file_id=modified_id,
1253
conflict_path=conflicting_path,
1254
conflict_file_id=conflicting_id)