1
# Copyright (C) 2006 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from stat import S_ISREG
21
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
22
ReusingTransform, NotVersionedError, CantMoveRoot,
23
ExistingLimbo, ImmortalLimbo)
24
from bzrlib.inventory import InventoryEntry
25
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
27
from bzrlib.progress import DummyProgress, ProgressPhase
28
from bzrlib.trace import mutter, warning
30
import bzrlib.urlutils as urlutils
33
ROOT_PARENT = "root-parent"
36
def unique_add(map, key, value):
38
raise DuplicateKey(key=key)
42
class _TransformResults(object):
43
def __init__(self, modified_paths):
45
self.modified_paths = modified_paths
48
class TreeTransform(object):
49
"""Represent a tree transformation.
51
This object is designed to support incremental generation of the transform,
54
It is easy to produce malformed transforms, but they are generally
55
harmless. Attempting to apply a malformed transform will cause an
56
exception to be raised before any modifications are made to the tree.
58
Many kinds of malformed transforms can be corrected with the
59
resolve_conflicts function. The remaining ones indicate programming error,
60
such as trying to create a file with no path.
62
Two sets of file creation methods are supplied. Convenience methods are:
67
These are composed of the low-level methods:
69
* create_file or create_directory or create_symlink
73
def __init__(self, tree, pb=DummyProgress()):
74
"""Note: a write lock is taken on the tree.
76
Use TreeTransform.finalize() to release the lock
80
self._tree.lock_write()
82
control_files = self._tree._control_files
83
self._limbodir = urlutils.local_path_from_url(
84
control_files.controlfilename('limbo'))
86
os.mkdir(self._limbodir)
88
if e.errno == errno.EEXIST:
89
raise ExistingLimbo(self._limbodir)
97
self._new_contents = {}
98
self._removed_contents = set()
99
self._new_executability = {}
101
self._non_present_ids = {}
103
self._removed_id = set()
104
self._tree_path_ids = {}
105
self._tree_id_paths = {}
107
# Cache of realpath results, to speed up canonical_path
109
# Cache of relpath results, to speed up canonical_path
110
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
114
def __get_root(self):
115
return self._new_root
117
root = property(__get_root)
120
"""Release the working tree lock, if held, clean up limbo dir."""
121
if self._tree is None:
124
for trans_id, kind in self._new_contents.iteritems():
125
path = self._limbo_name(trans_id)
126
if kind == "directory":
131
os.rmdir(self._limbodir)
133
# We don't especially care *why* the dir is immortal.
134
raise ImmortalLimbo(self._limbodir)
139
def _assign_id(self):
140
"""Produce a new tranform id"""
141
new_id = "new-%s" % self._id_number
145
def create_path(self, name, parent):
146
"""Assign a transaction id to a new path"""
147
trans_id = self._assign_id()
148
unique_add(self._new_name, trans_id, name)
149
unique_add(self._new_parent, trans_id, parent)
152
def adjust_path(self, name, parent, trans_id):
153
"""Change the path that is assigned to a transaction id."""
154
if trans_id == self._new_root:
156
self._new_name[trans_id] = name
157
self._new_parent[trans_id] = parent
159
def adjust_root_path(self, name, parent):
160
"""Emulate moving the root by moving all children, instead.
162
We do this by undoing the association of root's transaction id with the
163
current tree. This allows us to create a new directory with that
164
transaction id. We unversion the root directory and version the
165
physically new directory, and hope someone versions the tree root
168
old_root = self._new_root
169
old_root_file_id = self.final_file_id(old_root)
170
# force moving all children of root
171
for child_id in self.iter_tree_children(old_root):
172
if child_id != parent:
173
self.adjust_path(self.final_name(child_id),
174
self.final_parent(child_id), child_id)
175
file_id = self.final_file_id(child_id)
176
if file_id is not None:
177
self.unversion_file(child_id)
178
self.version_file(file_id, child_id)
180
# the physical root needs a new transaction id
181
self._tree_path_ids.pop("")
182
self._tree_id_paths.pop(old_root)
183
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
184
if parent == old_root:
185
parent = self._new_root
186
self.adjust_path(name, parent, old_root)
187
self.create_directory(old_root)
188
self.version_file(old_root_file_id, old_root)
189
self.unversion_file(self._new_root)
191
def trans_id_tree_file_id(self, inventory_id):
192
"""Determine the transaction id of a working tree file.
194
This reflects only files that already exist, not ones that will be
195
added by transactions.
197
path = self._tree.inventory.id2path(inventory_id)
198
return self.trans_id_tree_path(path)
200
def trans_id_file_id(self, file_id):
201
"""Determine or set the transaction id associated with a file ID.
202
A new id is only created for file_ids that were never present. If
203
a transaction has been unversioned, it is deliberately still returned.
204
(this will likely lead to an unversioned parent conflict.)
206
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
207
return self._r_new_id[file_id]
208
elif file_id in self._tree.inventory:
209
return self.trans_id_tree_file_id(file_id)
210
elif file_id in self._non_present_ids:
211
return self._non_present_ids[file_id]
213
trans_id = self._assign_id()
214
self._non_present_ids[file_id] = trans_id
217
def canonical_path(self, path):
218
"""Get the canonical tree-relative path"""
219
# don't follow final symlinks
220
abs = self._tree.abspath(path)
221
if abs in self._relpaths:
222
return self._relpaths[abs]
223
dirname, basename = os.path.split(abs)
224
if dirname not in self._realpaths:
225
self._realpaths[dirname] = os.path.realpath(dirname)
226
dirname = self._realpaths[dirname]
227
abs = pathjoin(dirname, basename)
228
if dirname in self._relpaths:
229
relpath = pathjoin(self._relpaths[dirname], basename)
230
relpath = relpath.rstrip('/\\')
232
relpath = self._tree.relpath(abs)
233
self._relpaths[abs] = relpath
236
def trans_id_tree_path(self, path):
237
"""Determine (and maybe set) the transaction ID for a tree path."""
238
path = self.canonical_path(path)
239
if path not in self._tree_path_ids:
240
self._tree_path_ids[path] = self._assign_id()
241
self._tree_id_paths[self._tree_path_ids[path]] = path
242
return self._tree_path_ids[path]
244
def get_tree_parent(self, trans_id):
245
"""Determine id of the parent in the tree."""
246
path = self._tree_id_paths[trans_id]
249
return self.trans_id_tree_path(os.path.dirname(path))
251
def create_file(self, contents, trans_id, mode_id=None):
252
"""Schedule creation of a new file.
256
Contents is an iterator of strings, all of which will be written
257
to the target destination.
259
New file takes the permissions of any existing file with that id,
260
unless mode_id is specified.
262
f = 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]
447
return os.path.basename(self._tree_id_paths[trans_id])
450
"""Return a map of parent: children for known parents.
452
Only new paths and parents of tree files with assigned ids are used.
455
items = list(self._new_parent.iteritems())
456
items.extend((t, self.final_parent(t)) for t in
457
self._tree_id_paths.keys())
458
for trans_id, parent_id in items:
459
if parent_id not in by_parent:
460
by_parent[parent_id] = set()
461
by_parent[parent_id].add(trans_id)
464
def path_changed(self, trans_id):
465
"""Return True if a trans_id's path has changed."""
466
return trans_id in self._new_name or trans_id in self._new_parent
468
def find_conflicts(self):
469
"""Find any violations of inventory or filesystem invariants"""
470
if self.__done is True:
471
raise ReusingTransform()
473
# ensure all children of all existent parents are known
474
# all children of non-existent parents are known, by definition.
475
self._add_tree_children()
476
by_parent = self.by_parent()
477
conflicts.extend(self._unversioned_parents(by_parent))
478
conflicts.extend(self._parent_loops())
479
conflicts.extend(self._duplicate_entries(by_parent))
480
conflicts.extend(self._duplicate_ids())
481
conflicts.extend(self._parent_type_conflicts(by_parent))
482
conflicts.extend(self._improper_versioning())
483
conflicts.extend(self._executability_conflicts())
484
conflicts.extend(self._overwrite_conflicts())
487
def _add_tree_children(self):
488
"""Add all the children of all active parents to the known paths.
490
Active parents are those which gain children, and those which are
491
removed. This is a necessary first step in detecting conflicts.
493
parents = self.by_parent().keys()
494
parents.extend([t for t in self._removed_contents if
495
self.tree_kind(t) == 'directory'])
496
for trans_id in self._removed_id:
497
file_id = self.tree_file_id(trans_id)
498
if self._tree.inventory[file_id].kind in ('directory',
500
parents.append(trans_id)
502
for parent_id in parents:
503
# ensure that all children are registered with the transaction
504
list(self.iter_tree_children(parent_id))
506
def iter_tree_children(self, parent_id):
507
"""Iterate through the entry's tree children, if any"""
509
path = self._tree_id_paths[parent_id]
513
children = os.listdir(self._tree.abspath(path))
515
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
519
for child in children:
520
childpath = joinpath(path, child)
521
if self._tree.is_control_filename(childpath):
523
yield self.trans_id_tree_path(childpath)
525
def has_named_child(self, by_parent, parent_id, name):
527
children = by_parent[parent_id]
530
for child in children:
531
if self.final_name(child) == name:
534
path = self._tree_id_paths[parent_id]
537
childpath = joinpath(path, name)
538
child_id = self._tree_path_ids.get(childpath)
540
return lexists(self._tree.abspath(childpath))
542
if tt.final_parent(child_id) != parent_id:
544
if child_id in tt._removed_contents:
545
# XXX What about dangling file-ids?
550
def _parent_loops(self):
551
"""No entry should be its own ancestor"""
553
for trans_id in self._new_parent:
556
while parent_id is not ROOT_PARENT:
558
parent_id = self.final_parent(parent_id)
559
if parent_id == trans_id:
560
conflicts.append(('parent loop', trans_id))
561
if parent_id in seen:
565
def _unversioned_parents(self, by_parent):
566
"""If parent directories are versioned, children must be versioned."""
568
for parent_id, children in by_parent.iteritems():
569
if parent_id is ROOT_PARENT:
571
if self.final_file_id(parent_id) is not None:
573
for child_id in children:
574
if self.final_file_id(child_id) is not None:
575
conflicts.append(('unversioned parent', parent_id))
579
def _improper_versioning(self):
580
"""Cannot version a file with no contents, or a bad type.
582
However, existing entries with no contents are okay.
585
for trans_id in self._new_id.iterkeys():
587
kind = self.final_kind(trans_id)
589
conflicts.append(('versioning no contents', trans_id))
591
if not InventoryEntry.versionable_kind(kind):
592
conflicts.append(('versioning bad kind', trans_id, kind))
595
def _executability_conflicts(self):
596
"""Check for bad executability changes.
598
Only versioned files may have their executability set, because
599
1. only versioned entries can have executability under windows
600
2. only files can be executable. (The execute bit on a directory
601
does not indicate searchability)
604
for trans_id in self._new_executability:
605
if self.final_file_id(trans_id) is None:
606
conflicts.append(('unversioned executability', trans_id))
609
non_file = self.final_kind(trans_id) != "file"
613
conflicts.append(('non-file executability', trans_id))
616
def _overwrite_conflicts(self):
617
"""Check for overwrites (not permitted on Win32)"""
619
for trans_id in self._new_contents:
621
self.tree_kind(trans_id)
624
if trans_id not in self._removed_contents:
625
conflicts.append(('overwrite', trans_id,
626
self.final_name(trans_id)))
629
def _duplicate_entries(self, by_parent):
630
"""No directory may have two entries with the same name."""
632
for children in by_parent.itervalues():
633
name_ids = [(self.final_name(t), t) for t in children]
637
for name, trans_id in name_ids:
638
if name == last_name:
639
conflicts.append(('duplicate', last_trans_id, trans_id,
642
kind = self.final_kind(trans_id)
645
file_id = self.final_file_id(trans_id)
646
if kind is not None or file_id is not None:
648
last_trans_id = trans_id
651
def _duplicate_ids(self):
652
"""Each inventory id may only be used once"""
654
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
656
active_tree_ids = set((f for f in self._tree.inventory if
657
f not in removed_tree_ids))
658
for trans_id, file_id in self._new_id.iteritems():
659
if file_id in active_tree_ids:
660
old_trans_id = self.trans_id_tree_file_id(file_id)
661
conflicts.append(('duplicate id', old_trans_id, trans_id))
664
def _parent_type_conflicts(self, by_parent):
665
"""parents must have directory 'contents'."""
667
for parent_id, children in by_parent.iteritems():
668
if parent_id is ROOT_PARENT:
670
if not self._any_contents(children):
672
for child in children:
674
self.final_kind(child)
678
kind = self.final_kind(parent_id)
682
conflicts.append(('missing parent', parent_id))
683
elif kind != "directory":
684
conflicts.append(('non-directory parent', parent_id))
687
def _any_contents(self, trans_ids):
688
"""Return true if any of the trans_ids, will have contents."""
689
for trans_id in trans_ids:
691
kind = self.final_kind(trans_id)
698
"""Apply all changes to the inventory and filesystem.
700
If filesystem or inventory conflicts are present, MalformedTransform
703
conflicts = self.find_conflicts()
704
if len(conflicts) != 0:
705
raise MalformedTransform(conflicts=conflicts)
707
inv = self._tree.inventory
708
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
710
child_pb.update('Apply phase', 0, 2)
711
self._apply_removals(inv, limbo_inv)
712
child_pb.update('Apply phase', 1, 2)
713
modified_paths = self._apply_insertions(inv, limbo_inv)
716
self._tree._write_inventory(inv)
719
return _TransformResults(modified_paths)
721
def _limbo_name(self, trans_id):
722
"""Generate the limbo name of a file"""
723
return pathjoin(self._limbodir, trans_id)
725
def _apply_removals(self, inv, limbo_inv):
726
"""Perform tree operations that remove directory/inventory names.
728
That is, delete files that are to be deleted, and put any files that
729
need renaming into limbo. This must be done in strict child-to-parent
732
tree_paths = list(self._tree_path_ids.iteritems())
733
tree_paths.sort(reverse=True)
734
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
736
for num, data in enumerate(tree_paths):
737
path, trans_id = data
738
child_pb.update('removing file', num, len(tree_paths))
739
full_path = self._tree.abspath(path)
740
if trans_id in self._removed_contents:
741
delete_any(full_path)
742
elif trans_id in self._new_name or trans_id in \
745
os.rename(full_path, self._limbo_name(trans_id))
747
if e.errno != errno.ENOENT:
749
if trans_id in self._removed_id:
750
if trans_id == self._new_root:
751
file_id = self._tree.inventory.root.file_id
753
file_id = self.tree_file_id(trans_id)
755
elif trans_id in self._new_name or trans_id in self._new_parent:
756
file_id = self.tree_file_id(trans_id)
757
if file_id is not None:
758
limbo_inv[trans_id] = inv[file_id]
763
def _apply_insertions(self, inv, limbo_inv):
764
"""Perform tree operations that insert directory/inventory names.
766
That is, create any files that need to be created, and restore from
767
limbo any files that needed renaming. This must be done in strict
768
parent-to-child order.
770
new_paths = self.new_paths()
772
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
774
for num, (path, trans_id) in enumerate(new_paths):
775
child_pb.update('adding file', num, len(new_paths))
777
kind = self._new_contents[trans_id]
779
kind = contents = None
780
if trans_id in self._new_contents or \
781
self.path_changed(trans_id):
782
full_path = self._tree.abspath(path)
784
os.rename(self._limbo_name(trans_id), full_path)
786
# We may be renaming a dangling inventory id
787
if e.errno != errno.ENOENT:
789
if trans_id in self._new_contents:
790
modified_paths.append(full_path)
791
del self._new_contents[trans_id]
793
if trans_id in self._new_id:
795
kind = file_kind(self._tree.abspath(path))
796
inv.add_path(path, kind, self._new_id[trans_id])
797
elif trans_id in self._new_name or trans_id in\
799
entry = limbo_inv.get(trans_id)
800
if entry is not None:
801
entry.name = self.final_name(trans_id)
802
parent_path = os.path.dirname(path)
804
self._tree.inventory.path2id(parent_path)
807
# requires files and inventory entries to be in place
808
if trans_id in self._new_executability:
809
self._set_executability(path, inv, trans_id)
812
return modified_paths
814
def _set_executability(self, path, inv, trans_id):
815
"""Set the executability of versioned files """
816
file_id = inv.path2id(path)
817
new_executability = self._new_executability[trans_id]
818
inv[file_id].executable = new_executability
819
if supports_executable():
820
abspath = self._tree.abspath(path)
821
current_mode = os.stat(abspath).st_mode
822
if new_executability:
825
to_mode = current_mode | (0100 & ~umask)
826
# Enable x-bit for others only if they can read it.
827
if current_mode & 0004:
828
to_mode |= 0001 & ~umask
829
if current_mode & 0040:
830
to_mode |= 0010 & ~umask
832
to_mode = current_mode & ~0111
833
os.chmod(abspath, to_mode)
835
def _new_entry(self, name, parent_id, file_id):
836
"""Helper function to create a new filesystem entry."""
837
trans_id = self.create_path(name, parent_id)
838
if file_id is not None:
839
self.version_file(file_id, trans_id)
842
def new_file(self, name, parent_id, contents, file_id=None,
844
"""Convenience method to create files.
846
name is the name of the file to create.
847
parent_id is the transaction id of the parent directory of the file.
848
contents is an iterator of bytestrings, which will be used to produce
850
:param file_id: The inventory ID of the file, if it is to be versioned.
851
:param executable: Only valid when a file_id has been supplied.
853
trans_id = self._new_entry(name, parent_id, file_id)
854
# TODO: rather than scheduling a set_executable call,
855
# have create_file create the file with the right mode.
856
self.create_file(contents, trans_id)
857
if executable is not None:
858
self.set_executability(executable, trans_id)
861
def new_directory(self, name, parent_id, file_id=None):
862
"""Convenience method to create directories.
864
name is the name of the directory to create.
865
parent_id is the transaction id of the parent directory of the
867
file_id is the inventory ID of the directory, if it is to be versioned.
869
trans_id = self._new_entry(name, parent_id, file_id)
870
self.create_directory(trans_id)
873
def new_symlink(self, name, parent_id, target, file_id=None):
874
"""Convenience method to create symbolic link.
876
name is the name of the symlink to create.
877
parent_id is the transaction id of the parent directory of the symlink.
878
target is a bytestring of the target of the symlink.
879
file_id is the inventory ID of the file, if it is to be versioned.
881
trans_id = self._new_entry(name, parent_id, file_id)
882
self.create_symlink(target, trans_id)
885
def joinpath(parent, child):
886
"""Join tree-relative paths, handling the tree root specially"""
887
if parent is None or parent == "":
890
return pathjoin(parent, child)
893
class FinalPaths(object):
894
"""Make path calculation cheap by memoizing paths.
896
The underlying tree must not be manipulated between calls, or else
897
the results will likely be incorrect.
899
def __init__(self, transform):
900
object.__init__(self)
901
self._known_paths = {}
902
self.transform = transform
904
def _determine_path(self, trans_id):
905
if trans_id == self.transform.root:
907
name = self.transform.final_name(trans_id)
908
parent_id = self.transform.final_parent(trans_id)
909
if parent_id == self.transform.root:
912
return pathjoin(self.get_path(parent_id), name)
914
def get_path(self, trans_id):
915
"""Find the final path associated with a trans_id"""
916
if trans_id not in self._known_paths:
917
self._known_paths[trans_id] = self._determine_path(trans_id)
918
return self._known_paths[trans_id]
920
def topology_sorted_ids(tree):
921
"""Determine the topological order of the ids in a tree"""
922
file_ids = list(tree)
923
file_ids.sort(key=tree.id2path)
926
def build_tree(tree, wt):
927
"""Create working tree for a branch, using a Transaction."""
929
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
930
pp = ProgressPhase("Build phase", 2, top_pb)
931
tt = TreeTransform(wt)
934
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
935
file_ids = topology_sorted_ids(tree)
936
pb = bzrlib.ui.ui_factory.nested_progress_bar()
938
for num, file_id in enumerate(file_ids):
939
pb.update("Building tree", num, len(file_ids))
940
entry = tree.inventory[file_id]
941
if entry.parent_id is None:
943
if entry.parent_id not in file_trans_id:
944
raise repr(entry.parent_id)
945
parent_id = file_trans_id[entry.parent_id]
946
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
956
def new_by_entry(tt, entry, parent_id, tree):
957
"""Create a new file according to its inventory entry"""
961
contents = tree.get_file(entry.file_id).readlines()
962
executable = tree.is_executable(entry.file_id)
963
return tt.new_file(name, parent_id, contents, entry.file_id,
965
elif kind == 'directory':
966
return tt.new_directory(name, parent_id, entry.file_id)
967
elif kind == 'symlink':
968
target = tree.get_symlink_target(entry.file_id)
969
return tt.new_symlink(name, parent_id, target, entry.file_id)
971
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
972
"""Create new file contents according to an inventory entry."""
973
if entry.kind == "file":
975
lines = tree.get_file(entry.file_id).readlines()
976
tt.create_file(lines, trans_id, mode_id=mode_id)
977
elif entry.kind == "symlink":
978
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
979
elif entry.kind == "directory":
980
tt.create_directory(trans_id)
982
def create_entry_executability(tt, entry, trans_id):
983
"""Set the executability of a trans_id according to an inventory entry"""
984
if entry.kind == "file":
985
tt.set_executability(entry.executable, trans_id)
988
def find_interesting(working_tree, target_tree, filenames):
989
"""Find the ids corresponding to specified filenames."""
991
interesting_ids = None
993
interesting_ids = set()
994
for tree_path in filenames:
996
for tree in (working_tree, target_tree):
997
file_id = tree.inventory.path2id(tree_path)
998
if file_id is not None:
999
interesting_ids.add(file_id)
1002
raise NotVersionedError(path=tree_path)
1003
return interesting_ids
1006
def change_entry(tt, file_id, working_tree, target_tree,
1007
trans_id_file_id, backups, trans_id, by_parent):
1008
"""Replace a file_id's contents with those from a target tree."""
1009
e_trans_id = trans_id_file_id(file_id)
1010
entry = target_tree.inventory[file_id]
1011
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1014
mode_id = e_trans_id
1017
tt.delete_contents(e_trans_id)
1019
parent_trans_id = trans_id_file_id(entry.parent_id)
1020
backup_name = get_backup_name(entry, by_parent,
1021
parent_trans_id, tt)
1022
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1023
tt.unversion_file(e_trans_id)
1024
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1025
tt.version_file(file_id, e_trans_id)
1026
trans_id[file_id] = e_trans_id
1027
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1028
create_entry_executability(tt, entry, e_trans_id)
1031
tt.set_executability(entry.executable, e_trans_id)
1032
if tt.final_name(e_trans_id) != entry.name:
1035
parent_id = tt.final_parent(e_trans_id)
1036
parent_file_id = tt.final_file_id(parent_id)
1037
if parent_file_id != entry.parent_id:
1042
parent_trans_id = trans_id_file_id(entry.parent_id)
1043
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1046
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1047
"""Produce a backup-style name that appears to be available"""
1051
yield "%s.~%d~" % (entry.name, counter)
1053
for name in name_gen():
1054
if not tt.has_named_child(by_parent, parent_trans_id, name):
1057
def _entry_changes(file_id, entry, working_tree):
1058
"""Determine in which ways the inventory entry has changed.
1060
Returns booleans: has_contents, content_mod, meta_mod
1061
has_contents means there are currently contents, but they differ
1062
contents_mod means contents need to be modified
1063
meta_mod means the metadata needs to be modified
1065
cur_entry = working_tree.inventory[file_id]
1067
working_kind = working_tree.kind(file_id)
1070
has_contents = False
1073
if has_contents is True:
1074
real_e_kind = entry.kind
1075
if real_e_kind == 'root_directory':
1076
real_e_kind = 'directory'
1077
if real_e_kind != working_kind:
1078
contents_mod, meta_mod = True, False
1080
cur_entry._read_tree_state(working_tree.id2path(file_id),
1082
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1083
cur_entry._forget_tree_state()
1084
return has_contents, contents_mod, meta_mod
1087
def revert(working_tree, target_tree, filenames, backups=False,
1088
pb=DummyProgress()):
1089
"""Revert a working tree's contents to those of a target tree."""
1090
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1091
def interesting(file_id):
1092
return interesting_ids is None or file_id in interesting_ids
1094
tt = TreeTransform(working_tree, pb)
1096
merge_modified = working_tree.merge_modified()
1098
def trans_id_file_id(file_id):
1100
return trans_id[file_id]
1102
return tt.trans_id_tree_file_id(file_id)
1104
pp = ProgressPhase("Revert phase", 4, pb)
1106
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1108
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1110
by_parent = tt.by_parent()
1111
for id_num, file_id in enumerate(sorted_interesting):
1112
child_pb.update("Reverting file", id_num+1,
1113
len(sorted_interesting))
1114
if file_id not in working_tree.inventory:
1115
entry = target_tree.inventory[file_id]
1116
parent_id = trans_id_file_id(entry.parent_id)
1117
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1118
trans_id[file_id] = e_trans_id
1120
backup_this = backups
1121
if file_id in merge_modified:
1123
del merge_modified[file_id]
1124
change_entry(tt, file_id, working_tree, target_tree,
1125
trans_id_file_id, backup_this, trans_id,
1130
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1131
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1133
for id_num, file_id in enumerate(wt_interesting):
1134
child_pb.update("New file check", id_num+1,
1135
len(sorted_interesting))
1136
if file_id not in target_tree:
1137
trans_id = tt.trans_id_tree_file_id(file_id)
1138
tt.unversion_file(trans_id)
1139
if file_id in merge_modified:
1140
tt.delete_contents(trans_id)
1141
del merge_modified[file_id]
1145
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1147
raw_conflicts = resolve_conflicts(tt, child_pb)
1150
conflicts = cook_conflicts(raw_conflicts, tt)
1151
for conflict in conflicts:
1155
working_tree.set_merge_modified({})
1162
def resolve_conflicts(tt, pb=DummyProgress()):
1163
"""Make many conflict-resolution attempts, but die if they fail"""
1164
new_conflicts = set()
1167
pb.update('Resolution pass', n+1, 10)
1168
conflicts = tt.find_conflicts()
1169
if len(conflicts) == 0:
1170
return new_conflicts
1171
new_conflicts.update(conflict_pass(tt, conflicts))
1172
raise MalformedTransform(conflicts=conflicts)
1177
def conflict_pass(tt, conflicts):
1178
"""Resolve some classes of conflicts."""
1179
new_conflicts = set()
1180
for c_type, conflict in ((c[0], c) for c in conflicts):
1181
if c_type == 'duplicate id':
1182
tt.unversion_file(conflict[1])
1183
new_conflicts.add((c_type, 'Unversioned existing file',
1184
conflict[1], conflict[2], ))
1185
elif c_type == 'duplicate':
1186
# files that were renamed take precedence
1187
new_name = tt.final_name(conflict[1])+'.moved'
1188
final_parent = tt.final_parent(conflict[1])
1189
if tt.path_changed(conflict[1]):
1190
tt.adjust_path(new_name, final_parent, conflict[2])
1191
new_conflicts.add((c_type, 'Moved existing file to',
1192
conflict[2], conflict[1]))
1194
tt.adjust_path(new_name, final_parent, conflict[1])
1195
new_conflicts.add((c_type, 'Moved existing file to',
1196
conflict[1], conflict[2]))
1197
elif c_type == 'parent loop':
1198
# break the loop by undoing one of the ops that caused the loop
1200
while not tt.path_changed(cur):
1201
cur = tt.final_parent(cur)
1202
new_conflicts.add((c_type, 'Cancelled move', cur,
1203
tt.final_parent(cur),))
1204
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1206
elif c_type == 'missing parent':
1207
trans_id = conflict[1]
1209
tt.cancel_deletion(trans_id)
1210
new_conflicts.add((c_type, 'Not deleting', trans_id))
1212
tt.create_directory(trans_id)
1213
new_conflicts.add((c_type, 'Created directory.', trans_id))
1214
elif c_type == 'unversioned parent':
1215
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1216
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1217
return new_conflicts
1220
def cook_conflicts(raw_conflicts, tt):
1221
"""Generate a list of cooked conflicts, sorted by file path"""
1222
from bzrlib.conflicts import Conflict
1223
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1224
return sorted(conflict_iter, key=Conflict.sort_key)
1227
def iter_cook_conflicts(raw_conflicts, tt):
1228
from bzrlib.conflicts import Conflict
1230
for conflict in raw_conflicts:
1231
c_type = conflict[0]
1232
action = conflict[1]
1233
modified_path = fp.get_path(conflict[2])
1234
modified_id = tt.final_file_id(conflict[2])
1235
if len(conflict) == 3:
1236
yield Conflict.factory(c_type, action=action, path=modified_path,
1237
file_id=modified_id)
1240
conflicting_path = fp.get_path(conflict[3])
1241
conflicting_id = tt.final_file_id(conflict[3])
1242
yield Conflict.factory(c_type, action=action, path=modified_path,
1243
file_id=modified_id,
1244
conflict_path=conflicting_path,
1245
conflict_file_id=conflicting_id)