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 import bzrdir, errors
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
24
from bzrlib import delta
26
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
27
ReusingTransform, NotVersionedError, CantMoveRoot,
28
ExistingLimbo, ImmortalLimbo, NoFinalPath)
29
from bzrlib.inventory import InventoryEntry
30
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
32
from bzrlib.progress import DummyProgress, ProgressPhase
33
from bzrlib.symbol_versioning import deprecated_function, zero_fifteen
34
from bzrlib.trace import mutter, warning
35
from bzrlib import tree
37
import bzrlib.urlutils as urlutils
40
ROOT_PARENT = "root-parent"
43
def unique_add(map, key, value):
45
raise DuplicateKey(key=key)
49
class _TransformResults(object):
50
def __init__(self, modified_paths):
52
self.modified_paths = modified_paths
55
class TreeTransform(object):
56
"""Represent a tree transformation.
58
This object is designed to support incremental generation of the transform,
61
It is easy to produce malformed transforms, but they are generally
62
harmless. Attempting to apply a malformed transform will cause an
63
exception to be raised before any modifications are made to the tree.
65
Many kinds of malformed transforms can be corrected with the
66
resolve_conflicts function. The remaining ones indicate programming error,
67
such as trying to create a file with no path.
69
Two sets of file creation methods are supplied. Convenience methods are:
74
These are composed of the low-level methods:
76
* create_file or create_directory or create_symlink
80
def __init__(self, tree, pb=DummyProgress()):
81
"""Note: a tree_write lock is taken on the tree.
83
Use TreeTransform.finalize() to release the lock
87
self._tree.lock_tree_write()
89
control_files = self._tree._control_files
90
self._limbodir = urlutils.local_path_from_url(
91
control_files.controlfilename('limbo'))
93
os.mkdir(self._limbodir)
95
if e.errno == errno.EEXIST:
96
raise ExistingLimbo(self._limbodir)
103
self._new_parent = {}
104
self._new_contents = {}
105
self._removed_contents = set()
106
self._new_executability = {}
108
self._non_present_ids = {}
110
self._removed_id = set()
111
self._tree_path_ids = {}
112
self._tree_id_paths = {}
114
# Cache of realpath results, to speed up canonical_path
116
# Cache of relpath results, to speed up canonical_path
117
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
121
def __get_root(self):
122
return self._new_root
124
root = property(__get_root)
127
"""Release the working tree lock, if held, clean up limbo dir."""
128
if self._tree is None:
131
for trans_id, kind in self._new_contents.iteritems():
132
path = self._limbo_name(trans_id)
133
if kind == "directory":
138
os.rmdir(self._limbodir)
140
# We don't especially care *why* the dir is immortal.
141
raise ImmortalLimbo(self._limbodir)
146
def _assign_id(self):
147
"""Produce a new tranform id"""
148
new_id = "new-%s" % self._id_number
152
def create_path(self, name, parent):
153
"""Assign a transaction id to a new path"""
154
trans_id = self._assign_id()
155
unique_add(self._new_name, trans_id, name)
156
unique_add(self._new_parent, trans_id, parent)
159
def adjust_path(self, name, parent, trans_id):
160
"""Change the path that is assigned to a transaction id."""
161
if trans_id == self._new_root:
163
self._new_name[trans_id] = name
164
self._new_parent[trans_id] = parent
166
def adjust_root_path(self, name, parent):
167
"""Emulate moving the root by moving all children, instead.
169
We do this by undoing the association of root's transaction id with the
170
current tree. This allows us to create a new directory with that
171
transaction id. We unversion the root directory and version the
172
physically new directory, and hope someone versions the tree root
175
old_root = self._new_root
176
old_root_file_id = self.final_file_id(old_root)
177
# force moving all children of root
178
for child_id in self.iter_tree_children(old_root):
179
if child_id != parent:
180
self.adjust_path(self.final_name(child_id),
181
self.final_parent(child_id), child_id)
182
file_id = self.final_file_id(child_id)
183
if file_id is not None:
184
self.unversion_file(child_id)
185
self.version_file(file_id, child_id)
187
# the physical root needs a new transaction id
188
self._tree_path_ids.pop("")
189
self._tree_id_paths.pop(old_root)
190
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
191
if parent == old_root:
192
parent = self._new_root
193
self.adjust_path(name, parent, old_root)
194
self.create_directory(old_root)
195
self.version_file(old_root_file_id, old_root)
196
self.unversion_file(self._new_root)
198
def trans_id_tree_file_id(self, inventory_id):
199
"""Determine the transaction id of a working tree file.
201
This reflects only files that already exist, not ones that will be
202
added by transactions.
204
path = self._tree.inventory.id2path(inventory_id)
205
return self.trans_id_tree_path(path)
207
def trans_id_file_id(self, file_id):
208
"""Determine or set the transaction id associated with a file ID.
209
A new id is only created for file_ids that were never present. If
210
a transaction has been unversioned, it is deliberately still returned.
211
(this will likely lead to an unversioned parent conflict.)
213
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
214
return self._r_new_id[file_id]
215
elif file_id in self._tree.inventory:
216
return self.trans_id_tree_file_id(file_id)
217
elif file_id in self._non_present_ids:
218
return self._non_present_ids[file_id]
220
trans_id = self._assign_id()
221
self._non_present_ids[file_id] = trans_id
224
def canonical_path(self, path):
225
"""Get the canonical tree-relative path"""
226
# don't follow final symlinks
227
abs = self._tree.abspath(path)
228
if abs in self._relpaths:
229
return self._relpaths[abs]
230
dirname, basename = os.path.split(abs)
231
if dirname not in self._realpaths:
232
self._realpaths[dirname] = os.path.realpath(dirname)
233
dirname = self._realpaths[dirname]
234
abs = pathjoin(dirname, basename)
235
if dirname in self._relpaths:
236
relpath = pathjoin(self._relpaths[dirname], basename)
237
relpath = relpath.rstrip('/\\')
239
relpath = self._tree.relpath(abs)
240
self._relpaths[abs] = relpath
243
def trans_id_tree_path(self, path):
244
"""Determine (and maybe set) the transaction ID for a tree path."""
245
path = self.canonical_path(path)
246
if path not in self._tree_path_ids:
247
self._tree_path_ids[path] = self._assign_id()
248
self._tree_id_paths[self._tree_path_ids[path]] = path
249
return self._tree_path_ids[path]
251
def get_tree_parent(self, trans_id):
252
"""Determine id of the parent in the tree."""
253
path = self._tree_id_paths[trans_id]
256
return self.trans_id_tree_path(os.path.dirname(path))
258
def create_file(self, contents, trans_id, mode_id=None):
259
"""Schedule creation of a new file.
263
Contents is an iterator of strings, all of which will be written
264
to the target destination.
266
New file takes the permissions of any existing file with that id,
267
unless mode_id is specified.
269
name = self._limbo_name(trans_id)
273
unique_add(self._new_contents, trans_id, 'file')
275
# Clean up the file, it never got registered so
276
# TreeTransform.finalize() won't clean it up.
281
f.writelines(contents)
284
self._set_mode(trans_id, mode_id, S_ISREG)
286
def _set_mode(self, trans_id, mode_id, typefunc):
287
"""Set the mode of new file contents.
288
The mode_id is the existing file to get the mode from (often the same
289
as trans_id). The operation is only performed if there's a mode match
290
according to typefunc.
295
old_path = self._tree_id_paths[mode_id]
299
mode = os.stat(self._tree.abspath(old_path)).st_mode
301
if e.errno == errno.ENOENT:
306
os.chmod(self._limbo_name(trans_id), mode)
308
def create_directory(self, trans_id):
309
"""Schedule creation of a new directory.
311
See also new_directory.
313
os.mkdir(self._limbo_name(trans_id))
314
unique_add(self._new_contents, trans_id, 'directory')
316
def create_symlink(self, target, trans_id):
317
"""Schedule creation of a new symbolic link.
319
target is a bytestring.
320
See also new_symlink.
322
os.symlink(target, self._limbo_name(trans_id))
323
unique_add(self._new_contents, trans_id, 'symlink')
325
def cancel_creation(self, trans_id):
326
"""Cancel the creation of new file contents."""
327
del self._new_contents[trans_id]
328
delete_any(self._limbo_name(trans_id))
330
def delete_contents(self, trans_id):
331
"""Schedule the contents of a path entry for deletion"""
332
self.tree_kind(trans_id)
333
self._removed_contents.add(trans_id)
335
def cancel_deletion(self, trans_id):
336
"""Cancel a scheduled deletion"""
337
self._removed_contents.remove(trans_id)
339
def unversion_file(self, trans_id):
340
"""Schedule a path entry to become unversioned"""
341
self._removed_id.add(trans_id)
343
def delete_versioned(self, trans_id):
344
"""Delete and unversion a versioned file"""
345
self.delete_contents(trans_id)
346
self.unversion_file(trans_id)
348
def set_executability(self, executability, trans_id):
349
"""Schedule setting of the 'execute' bit
350
To unschedule, set to None
352
if executability is None:
353
del self._new_executability[trans_id]
355
unique_add(self._new_executability, trans_id, executability)
357
def version_file(self, file_id, trans_id):
358
"""Schedule a file to become versioned."""
359
assert file_id is not None
360
unique_add(self._new_id, trans_id, file_id)
361
unique_add(self._r_new_id, file_id, trans_id)
363
def cancel_versioning(self, trans_id):
364
"""Undo a previous versioning of a file"""
365
file_id = self._new_id[trans_id]
366
del self._new_id[trans_id]
367
del self._r_new_id[file_id]
370
"""Determine the paths of all new and changed files"""
372
fp = FinalPaths(self)
373
for id_set in (self._new_name, self._new_parent, self._new_contents,
374
self._new_id, self._new_executability):
375
new_ids.update(id_set)
376
new_paths = [(fp.get_path(t), t) for t in new_ids]
380
def tree_kind(self, trans_id):
381
"""Determine the file kind in the working tree.
383
Raises NoSuchFile if the file does not exist
385
path = self._tree_id_paths.get(trans_id)
387
raise NoSuchFile(None)
389
return file_kind(self._tree.abspath(path))
391
if e.errno != errno.ENOENT:
394
raise NoSuchFile(path)
396
def final_kind(self, trans_id):
397
"""Determine the final file kind, after any changes applied.
399
Raises NoSuchFile if the file does not exist/has no contents.
400
(It is conceivable that a path would be created without the
401
corresponding contents insertion command)
403
if trans_id in self._new_contents:
404
return self._new_contents[trans_id]
405
elif trans_id in self._removed_contents:
406
raise NoSuchFile(None)
408
return self.tree_kind(trans_id)
410
def tree_file_id(self, trans_id):
411
"""Determine the file id associated with the trans_id in the tree"""
413
path = self._tree_id_paths[trans_id]
415
# the file is a new, unversioned file, or invalid trans_id
417
# the file is old; the old id is still valid
418
if self._new_root == trans_id:
419
return self._tree.inventory.root.file_id
420
return self._tree.inventory.path2id(path)
422
def final_file_id(self, trans_id):
423
"""Determine the file id after any changes are applied, or None.
425
None indicates that the file will not be versioned after changes are
429
# there is a new id for this file
430
assert self._new_id[trans_id] is not None
431
return self._new_id[trans_id]
433
if trans_id in self._removed_id:
435
return self.tree_file_id(trans_id)
437
def inactive_file_id(self, trans_id):
438
"""Return the inactive file_id associated with a transaction id.
439
That is, the one in the tree or in non_present_ids.
440
The file_id may actually be active, too.
442
file_id = self.tree_file_id(trans_id)
443
if file_id is not None:
445
for key, value in self._non_present_ids.iteritems():
446
if value == trans_id:
449
def final_parent(self, trans_id):
450
"""Determine the parent file_id, after any changes are applied.
452
ROOT_PARENT is returned for the tree root.
455
return self._new_parent[trans_id]
457
return self.get_tree_parent(trans_id)
459
def final_name(self, trans_id):
460
"""Determine the final filename, after all changes are applied."""
462
return self._new_name[trans_id]
465
return os.path.basename(self._tree_id_paths[trans_id])
467
raise NoFinalPath(trans_id, self)
470
"""Return a map of parent: children for known parents.
472
Only new paths and parents of tree files with assigned ids are used.
475
items = list(self._new_parent.iteritems())
476
items.extend((t, self.final_parent(t)) for t in
477
self._tree_id_paths.keys())
478
for trans_id, parent_id in items:
479
if parent_id not in by_parent:
480
by_parent[parent_id] = set()
481
by_parent[parent_id].add(trans_id)
484
def path_changed(self, trans_id):
485
"""Return True if a trans_id's path has changed."""
486
return (trans_id in self._new_name) or (trans_id in self._new_parent)
488
def new_contents(self, trans_id):
489
return (trans_id in self._new_contents)
491
def find_conflicts(self):
492
"""Find any violations of inventory or filesystem invariants"""
493
if self.__done is True:
494
raise ReusingTransform()
496
# ensure all children of all existent parents are known
497
# all children of non-existent parents are known, by definition.
498
self._add_tree_children()
499
by_parent = self.by_parent()
500
conflicts.extend(self._unversioned_parents(by_parent))
501
conflicts.extend(self._parent_loops())
502
conflicts.extend(self._duplicate_entries(by_parent))
503
conflicts.extend(self._duplicate_ids())
504
conflicts.extend(self._parent_type_conflicts(by_parent))
505
conflicts.extend(self._improper_versioning())
506
conflicts.extend(self._executability_conflicts())
507
conflicts.extend(self._overwrite_conflicts())
510
def _add_tree_children(self):
511
"""Add all the children of all active parents to the known paths.
513
Active parents are those which gain children, and those which are
514
removed. This is a necessary first step in detecting conflicts.
516
parents = self.by_parent().keys()
517
parents.extend([t for t in self._removed_contents if
518
self.tree_kind(t) == 'directory'])
519
for trans_id in self._removed_id:
520
file_id = self.tree_file_id(trans_id)
521
if self._tree.inventory[file_id].kind == 'directory':
522
parents.append(trans_id)
524
for parent_id in parents:
525
# ensure that all children are registered with the transaction
526
list(self.iter_tree_children(parent_id))
528
def iter_tree_children(self, parent_id):
529
"""Iterate through the entry's tree children, if any"""
531
path = self._tree_id_paths[parent_id]
535
children = os.listdir(self._tree.abspath(path))
537
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
541
for child in children:
542
childpath = joinpath(path, child)
543
if self._tree.is_control_filename(childpath):
545
yield self.trans_id_tree_path(childpath)
547
def has_named_child(self, by_parent, parent_id, name):
549
children = by_parent[parent_id]
552
for child in children:
553
if self.final_name(child) == name:
556
path = self._tree_id_paths[parent_id]
559
childpath = joinpath(path, name)
560
child_id = self._tree_path_ids.get(childpath)
562
return lexists(self._tree.abspath(childpath))
564
if self.final_parent(child_id) != parent_id:
566
if child_id in self._removed_contents:
567
# XXX What about dangling file-ids?
572
def _parent_loops(self):
573
"""No entry should be its own ancestor"""
575
for trans_id in self._new_parent:
578
while parent_id is not ROOT_PARENT:
581
parent_id = self.final_parent(parent_id)
584
if parent_id == trans_id:
585
conflicts.append(('parent loop', trans_id))
586
if parent_id in seen:
590
def _unversioned_parents(self, by_parent):
591
"""If parent directories are versioned, children must be versioned."""
593
for parent_id, children in by_parent.iteritems():
594
if parent_id is ROOT_PARENT:
596
if self.final_file_id(parent_id) is not None:
598
for child_id in children:
599
if self.final_file_id(child_id) is not None:
600
conflicts.append(('unversioned parent', parent_id))
604
def _improper_versioning(self):
605
"""Cannot version a file with no contents, or a bad type.
607
However, existing entries with no contents are okay.
610
for trans_id in self._new_id.iterkeys():
612
kind = self.final_kind(trans_id)
614
conflicts.append(('versioning no contents', trans_id))
616
if not InventoryEntry.versionable_kind(kind):
617
conflicts.append(('versioning bad kind', trans_id, kind))
620
def _executability_conflicts(self):
621
"""Check for bad executability changes.
623
Only versioned files may have their executability set, because
624
1. only versioned entries can have executability under windows
625
2. only files can be executable. (The execute bit on a directory
626
does not indicate searchability)
629
for trans_id in self._new_executability:
630
if self.final_file_id(trans_id) is None:
631
conflicts.append(('unversioned executability', trans_id))
634
non_file = self.final_kind(trans_id) != "file"
638
conflicts.append(('non-file executability', trans_id))
641
def _overwrite_conflicts(self):
642
"""Check for overwrites (not permitted on Win32)"""
644
for trans_id in self._new_contents:
646
self.tree_kind(trans_id)
649
if trans_id not in self._removed_contents:
650
conflicts.append(('overwrite', trans_id,
651
self.final_name(trans_id)))
654
def _duplicate_entries(self, by_parent):
655
"""No directory may have two entries with the same name."""
657
for children in by_parent.itervalues():
658
name_ids = [(self.final_name(t), t) for t in children]
662
for name, trans_id in name_ids:
664
kind = self.final_kind(trans_id)
667
file_id = self.final_file_id(trans_id)
668
if kind is None and file_id is None:
670
if name == last_name:
671
conflicts.append(('duplicate', last_trans_id, trans_id,
674
last_trans_id = trans_id
677
def _duplicate_ids(self):
678
"""Each inventory id may only be used once"""
680
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
682
active_tree_ids = set((f for f in self._tree.inventory if
683
f not in removed_tree_ids))
684
for trans_id, file_id in self._new_id.iteritems():
685
if file_id in active_tree_ids:
686
old_trans_id = self.trans_id_tree_file_id(file_id)
687
conflicts.append(('duplicate id', old_trans_id, trans_id))
690
def _parent_type_conflicts(self, by_parent):
691
"""parents must have directory 'contents'."""
693
for parent_id, children in by_parent.iteritems():
694
if parent_id is ROOT_PARENT:
696
if not self._any_contents(children):
698
for child in children:
700
self.final_kind(child)
704
kind = self.final_kind(parent_id)
708
conflicts.append(('missing parent', parent_id))
709
elif kind != "directory":
710
conflicts.append(('non-directory parent', parent_id))
713
def _any_contents(self, trans_ids):
714
"""Return true if any of the trans_ids, will have contents."""
715
for trans_id in trans_ids:
717
kind = self.final_kind(trans_id)
724
"""Apply all changes to the inventory and filesystem.
726
If filesystem or inventory conflicts are present, MalformedTransform
729
conflicts = self.find_conflicts()
730
if len(conflicts) != 0:
731
raise MalformedTransform(conflicts=conflicts)
733
inv = self._tree.inventory
734
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
736
child_pb.update('Apply phase', 0, 2)
737
self._apply_removals(inv, limbo_inv)
738
child_pb.update('Apply phase', 1, 2)
739
modified_paths = self._apply_insertions(inv, limbo_inv)
742
self._tree._write_inventory(inv)
745
return _TransformResults(modified_paths)
747
def _limbo_name(self, trans_id):
748
"""Generate the limbo name of a file"""
749
return pathjoin(self._limbodir, trans_id)
751
def _apply_removals(self, inv, limbo_inv):
752
"""Perform tree operations that remove directory/inventory names.
754
That is, delete files that are to be deleted, and put any files that
755
need renaming into limbo. This must be done in strict child-to-parent
758
tree_paths = list(self._tree_path_ids.iteritems())
759
tree_paths.sort(reverse=True)
760
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
762
for num, data in enumerate(tree_paths):
763
path, trans_id = data
764
child_pb.update('removing file', num, len(tree_paths))
765
full_path = self._tree.abspath(path)
766
if trans_id in self._removed_contents:
767
delete_any(full_path)
768
elif trans_id in self._new_name or trans_id in \
771
os.rename(full_path, self._limbo_name(trans_id))
773
if e.errno != errno.ENOENT:
775
if trans_id in self._removed_id:
776
if trans_id == self._new_root:
777
file_id = self._tree.inventory.root.file_id
779
file_id = self.tree_file_id(trans_id)
781
elif trans_id in self._new_name or trans_id in self._new_parent:
782
file_id = self.tree_file_id(trans_id)
783
if file_id is not None:
784
limbo_inv[trans_id] = inv[file_id]
789
def _apply_insertions(self, inv, limbo_inv):
790
"""Perform tree operations that insert directory/inventory names.
792
That is, create any files that need to be created, and restore from
793
limbo any files that needed renaming. This must be done in strict
794
parent-to-child order.
796
new_paths = self.new_paths()
798
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
800
for num, (path, trans_id) in enumerate(new_paths):
801
child_pb.update('adding file', num, len(new_paths))
803
kind = self._new_contents[trans_id]
805
kind = contents = None
806
if trans_id in self._new_contents or \
807
self.path_changed(trans_id):
808
full_path = self._tree.abspath(path)
810
os.rename(self._limbo_name(trans_id), full_path)
812
# We may be renaming a dangling inventory id
813
if e.errno != errno.ENOENT:
815
if trans_id in self._new_contents:
816
modified_paths.append(full_path)
817
del self._new_contents[trans_id]
819
if trans_id in self._new_id:
821
kind = file_kind(self._tree.abspath(path))
822
inv.add_path(path, kind, self._new_id[trans_id])
823
elif trans_id in self._new_name or trans_id in\
825
entry = limbo_inv.get(trans_id)
826
if entry is not None:
827
entry.name = self.final_name(trans_id)
828
parent_path = os.path.dirname(path)
830
self._tree.inventory.path2id(parent_path)
833
# requires files and inventory entries to be in place
834
if trans_id in self._new_executability:
835
self._set_executability(path, inv, trans_id)
838
return modified_paths
840
def _set_executability(self, path, inv, trans_id):
841
"""Set the executability of versioned files """
842
file_id = inv.path2id(path)
843
new_executability = self._new_executability[trans_id]
844
inv[file_id].executable = new_executability
845
if supports_executable():
846
abspath = self._tree.abspath(path)
847
current_mode = os.stat(abspath).st_mode
848
if new_executability:
851
to_mode = current_mode | (0100 & ~umask)
852
# Enable x-bit for others only if they can read it.
853
if current_mode & 0004:
854
to_mode |= 0001 & ~umask
855
if current_mode & 0040:
856
to_mode |= 0010 & ~umask
858
to_mode = current_mode & ~0111
859
os.chmod(abspath, to_mode)
861
def _new_entry(self, name, parent_id, file_id):
862
"""Helper function to create a new filesystem entry."""
863
trans_id = self.create_path(name, parent_id)
864
if file_id is not None:
865
self.version_file(file_id, trans_id)
868
def new_file(self, name, parent_id, contents, file_id=None,
870
"""Convenience method to create files.
872
name is the name of the file to create.
873
parent_id is the transaction id of the parent directory of the file.
874
contents is an iterator of bytestrings, which will be used to produce
876
:param file_id: The inventory ID of the file, if it is to be versioned.
877
:param executable: Only valid when a file_id has been supplied.
879
trans_id = self._new_entry(name, parent_id, file_id)
880
# TODO: rather than scheduling a set_executable call,
881
# have create_file create the file with the right mode.
882
self.create_file(contents, trans_id)
883
if executable is not None:
884
self.set_executability(executable, trans_id)
887
def new_directory(self, name, parent_id, file_id=None):
888
"""Convenience method to create directories.
890
name is the name of the directory to create.
891
parent_id is the transaction id of the parent directory of the
893
file_id is the inventory ID of the directory, if it is to be versioned.
895
trans_id = self._new_entry(name, parent_id, file_id)
896
self.create_directory(trans_id)
899
def new_symlink(self, name, parent_id, target, file_id=None):
900
"""Convenience method to create symbolic link.
902
name is the name of the symlink to create.
903
parent_id is the transaction id of the parent directory of the symlink.
904
target is a bytestring of the target of the symlink.
905
file_id is the inventory ID of the file, if it is to be versioned.
907
trans_id = self._new_entry(name, parent_id, file_id)
908
self.create_symlink(target, trans_id)
911
def _affected_ids(self):
912
"""Return the set of transform ids affected by the transform"""
913
trans_ids = set(self._removed_id)
914
trans_ids.update(self._new_id.keys())
915
trans_ids.update(self._removed_contents)
916
trans_ids.update(self._new_contents.keys())
917
trans_ids.update(self._new_executability.keys())
918
trans_ids.update(self._new_name.keys())
919
trans_ids.update(self._new_parent.keys())
922
def _get_file_id_maps(self):
923
"""Return mapping of file_ids to trans_ids in the to and from states"""
924
trans_ids = self._affected_ids()
927
# Build up two dicts: trans_ids associated with file ids in the
928
# FROM state, vs the TO state.
929
for trans_id in trans_ids:
930
from_file_id = self.tree_file_id(trans_id)
931
if from_file_id is not None:
932
from_trans_ids[from_file_id] = trans_id
933
to_file_id = self.final_file_id(trans_id)
934
if to_file_id is not None:
935
to_trans_ids[to_file_id] = trans_id
936
return from_trans_ids, to_trans_ids
938
def _from_file_data(self, from_trans_id, from_versioned, file_id):
939
"""Get data about a file in the from (tree) state
941
Return a (name, parent, kind, executable) tuple
943
from_path = self._tree_id_paths.get(from_trans_id)
945
# get data from working tree if versioned
946
from_entry = self._tree.inventory[file_id]
947
from_name = from_entry.name
948
from_parent = from_entry.parent_id
951
if from_path is None:
952
# File does not exist in FROM state
956
# File exists, but is not versioned. Have to use path-
958
from_name = os.path.basename(from_path)
959
tree_parent = self.get_tree_parent(from_trans_id)
960
from_parent = self.tree_file_id(tree_parent)
961
if from_path is not None:
962
from_kind, from_executable, from_stats = \
963
self._tree._comparison_data(from_entry, from_path)
966
from_executable = False
967
return from_name, from_parent, from_kind, from_executable
969
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
970
"""Get data about a file in the to (target) state
972
Return a (name, parent, kind, executable) tuple
974
to_name = self.final_name(to_trans_id)
976
to_kind = self.final_kind(to_trans_id)
979
to_parent = self.final_file_id(self.final_parent(to_trans_id))
980
if to_trans_id in self._new_executability:
981
to_executable = self._new_executability[to_trans_id]
982
elif to_trans_id == from_trans_id:
983
to_executable = from_executable
985
to_executable = False
986
return to_name, to_parent, to_kind, to_executable
988
def _iter_changes(self):
989
"""Produce output in the same format as Tree._iter_changes.
991
Will produce nonsensical results if invoked while inventory/filesystem
992
conflicts (as reported by TreeTransform.find_conflicts()) are present.
994
This reads the Transform, but only reproduces changes involving a
995
file_id. Files that are not versioned in either of the FROM or TO
996
states are not reflected.
998
final_paths = FinalPaths(self)
999
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1001
# Now iterate through all active file_ids
1002
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1004
from_trans_id = from_trans_ids.get(file_id)
1005
# find file ids, and determine versioning state
1006
if from_trans_id is None:
1007
from_versioned = False
1008
from_trans_id = to_trans_ids[file_id]
1010
from_versioned = True
1011
to_trans_id = to_trans_ids.get(file_id)
1012
if to_trans_id is None:
1013
to_versioned = False
1014
to_trans_id = from_trans_id
1018
from_name, from_parent, from_kind, from_executable = \
1019
self._from_file_data(from_trans_id, from_versioned, file_id)
1021
to_name, to_parent, to_kind, to_executable = \
1022
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1024
if not from_versioned:
1027
from_path = self._tree_id_paths.get(from_trans_id)
1028
if not to_versioned:
1031
to_path = final_paths.get_path(to_trans_id)
1032
if from_kind != to_kind:
1034
elif to_kind in ('file' or 'symlink') and (
1035
to_trans_id != from_trans_id or
1036
to_trans_id in self._new_contents):
1038
if (not modified and from_versioned == to_versioned and
1039
from_parent==to_parent and from_name == to_name and
1040
from_executable == to_executable):
1042
results.append((file_id, (from_path, to_path), modified,
1043
(from_versioned, to_versioned),
1044
(from_parent, to_parent),
1045
(from_name, to_name),
1046
(from_kind, to_kind),
1047
(from_executable, to_executable)))
1048
return iter(sorted(results, key=lambda x:x[1]))
1051
def joinpath(parent, child):
1052
"""Join tree-relative paths, handling the tree root specially"""
1053
if parent is None or parent == "":
1056
return pathjoin(parent, child)
1059
class FinalPaths(object):
1060
"""Make path calculation cheap by memoizing paths.
1062
The underlying tree must not be manipulated between calls, or else
1063
the results will likely be incorrect.
1065
def __init__(self, transform):
1066
object.__init__(self)
1067
self._known_paths = {}
1068
self.transform = transform
1070
def _determine_path(self, trans_id):
1071
if trans_id == self.transform.root:
1073
name = self.transform.final_name(trans_id)
1074
parent_id = self.transform.final_parent(trans_id)
1075
if parent_id == self.transform.root:
1078
return pathjoin(self.get_path(parent_id), name)
1080
def get_path(self, trans_id):
1081
"""Find the final path associated with a trans_id"""
1082
if trans_id not in self._known_paths:
1083
self._known_paths[trans_id] = self._determine_path(trans_id)
1084
return self._known_paths[trans_id]
1086
def topology_sorted_ids(tree):
1087
"""Determine the topological order of the ids in a tree"""
1088
file_ids = list(tree)
1089
file_ids.sort(key=tree.id2path)
1093
def build_tree(tree, wt):
1094
"""Create working tree for a branch, using a TreeTransform.
1096
This function should be used on empty trees, having a tree root at most.
1097
(see merge and revert functionality for working with existing trees)
1099
Existing files are handled like so:
1101
- Existing bzrdirs take precedence over creating new items. They are
1102
created as '%s.diverted' % name.
1103
- Otherwise, if the content on disk matches the content we are building,
1104
it is silently replaced.
1105
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1107
wt.lock_tree_write()
1111
return _build_tree(tree, wt)
1117
def _build_tree(tree, wt):
1118
"""See build_tree."""
1119
if len(wt.inventory) > 1: # more than just a root
1120
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1122
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1123
pp = ProgressPhase("Build phase", 2, top_pb)
1124
# if tree.inventory.root is not None:
1125
# wt.set_root_id(tree.inventory.root.file_id)
1126
tt = TreeTransform(wt)
1130
file_trans_id[wt.get_root_id()] = \
1131
tt.trans_id_tree_file_id(wt.get_root_id())
1132
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1134
for num, (tree_path, entry) in \
1135
enumerate(tree.inventory.iter_entries_by_dir()):
1136
pb.update("Building tree", num, len(tree.inventory))
1137
if entry.parent_id is None:
1140
file_id = entry.file_id
1141
target_path = wt.abspath(tree_path)
1143
kind = file_kind(target_path)
1147
if kind == "directory":
1149
bzrdir.BzrDir.open(target_path)
1150
except errors.NotBranchError:
1154
if (file_id not in divert and
1155
_content_match(tree, entry, file_id, kind,
1157
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1158
if kind == 'directory':
1160
if entry.parent_id not in file_trans_id:
1161
raise repr(entry.parent_id)
1162
parent_id = file_trans_id[entry.parent_id]
1163
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1166
new_trans_id = file_trans_id[file_id]
1167
old_parent = tt.trans_id_tree_path(tree_path)
1168
_reparent_children(tt, old_parent, new_trans_id)
1172
divert_trans = set(file_trans_id[f] for f in divert)
1173
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1174
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1175
conflicts = cook_conflicts(raw_conflicts, tt)
1176
for conflict in conflicts:
1179
wt.add_conflicts(conflicts)
1180
except errors.UnsupportedOperation:
1188
def _reparent_children(tt, old_parent, new_parent):
1189
for child in tt.iter_tree_children(old_parent):
1190
tt.adjust_path(tt.final_name(child), new_parent, child)
1193
def _content_match(tree, entry, file_id, kind, target_path):
1194
if entry.kind != kind:
1196
if entry.kind == "directory":
1198
if entry.kind == "file":
1199
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1201
elif entry.kind == "symlink":
1202
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1207
def resolve_checkout(tt, conflicts, divert):
1208
new_conflicts = set()
1209
for c_type, conflict in ((c[0], c) for c in conflicts):
1210
# Anything but a 'duplicate' would indicate programmer error
1211
assert c_type == 'duplicate', c_type
1212
# Now figure out which is new and which is old
1213
if tt.new_contents(conflict[1]):
1214
new_file = conflict[1]
1215
old_file = conflict[2]
1217
new_file = conflict[2]
1218
old_file = conflict[1]
1220
# We should only get here if the conflict wasn't completely
1222
final_parent = tt.final_parent(old_file)
1223
if new_file in divert:
1224
new_name = tt.final_name(old_file)+'.diverted'
1225
tt.adjust_path(new_name, final_parent, new_file)
1226
new_conflicts.add((c_type, 'Diverted to',
1227
new_file, old_file))
1229
new_name = tt.final_name(old_file)+'.moved'
1230
tt.adjust_path(new_name, final_parent, old_file)
1231
new_conflicts.add((c_type, 'Moved existing file to',
1232
old_file, new_file))
1233
return new_conflicts
1236
def new_by_entry(tt, entry, parent_id, tree):
1237
"""Create a new file according to its inventory entry"""
1241
contents = tree.get_file(entry.file_id).readlines()
1242
executable = tree.is_executable(entry.file_id)
1243
return tt.new_file(name, parent_id, contents, entry.file_id,
1245
elif kind == 'directory':
1246
return tt.new_directory(name, parent_id, entry.file_id)
1247
elif kind == 'symlink':
1248
target = tree.get_symlink_target(entry.file_id)
1249
return tt.new_symlink(name, parent_id, target, entry.file_id)
1251
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1252
"""Create new file contents according to an inventory entry."""
1253
if entry.kind == "file":
1255
lines = tree.get_file(entry.file_id).readlines()
1256
tt.create_file(lines, trans_id, mode_id=mode_id)
1257
elif entry.kind == "symlink":
1258
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1259
elif entry.kind == "directory":
1260
tt.create_directory(trans_id)
1262
def create_entry_executability(tt, entry, trans_id):
1263
"""Set the executability of a trans_id according to an inventory entry"""
1264
if entry.kind == "file":
1265
tt.set_executability(entry.executable, trans_id)
1268
@deprecated_function(zero_fifteen)
1269
def find_interesting(working_tree, target_tree, filenames):
1270
"""Find the ids corresponding to specified filenames.
1272
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1274
working_tree.lock_read()
1276
target_tree.lock_read()
1278
return working_tree.paths2ids(filenames, [target_tree])
1280
target_tree.unlock()
1282
working_tree.unlock()
1285
def change_entry(tt, file_id, working_tree, target_tree,
1286
trans_id_file_id, backups, trans_id, by_parent):
1287
"""Replace a file_id's contents with those from a target tree."""
1288
e_trans_id = trans_id_file_id(file_id)
1289
entry = target_tree.inventory[file_id]
1290
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1293
mode_id = e_trans_id
1296
tt.delete_contents(e_trans_id)
1298
parent_trans_id = trans_id_file_id(entry.parent_id)
1299
backup_name = get_backup_name(entry, by_parent,
1300
parent_trans_id, tt)
1301
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1302
tt.unversion_file(e_trans_id)
1303
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1304
tt.version_file(file_id, e_trans_id)
1305
trans_id[file_id] = e_trans_id
1306
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1307
create_entry_executability(tt, entry, e_trans_id)
1310
tt.set_executability(entry.executable, e_trans_id)
1311
if tt.final_name(e_trans_id) != entry.name:
1314
parent_id = tt.final_parent(e_trans_id)
1315
parent_file_id = tt.final_file_id(parent_id)
1316
if parent_file_id != entry.parent_id:
1321
parent_trans_id = trans_id_file_id(entry.parent_id)
1322
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1325
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1326
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1329
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1330
"""Produce a backup-style name that appears to be available"""
1334
yield "%s.~%d~" % (name, counter)
1336
for new_name in name_gen():
1337
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1341
def _entry_changes(file_id, entry, working_tree):
1342
"""Determine in which ways the inventory entry has changed.
1344
Returns booleans: has_contents, content_mod, meta_mod
1345
has_contents means there are currently contents, but they differ
1346
contents_mod means contents need to be modified
1347
meta_mod means the metadata needs to be modified
1349
cur_entry = working_tree.inventory[file_id]
1351
working_kind = working_tree.kind(file_id)
1354
has_contents = False
1357
if has_contents is True:
1358
if entry.kind != working_kind:
1359
contents_mod, meta_mod = True, False
1361
cur_entry._read_tree_state(working_tree.id2path(file_id),
1363
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1364
cur_entry._forget_tree_state()
1365
return has_contents, contents_mod, meta_mod
1368
def revert(working_tree, target_tree, filenames, backups=False,
1369
pb=DummyProgress(), change_reporter=None):
1370
"""Revert a working tree's contents to those of a target tree."""
1371
target_tree.lock_read()
1372
tt = TreeTransform(working_tree, pb)
1374
pp = ProgressPhase("Revert phase", 3, pb)
1376
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1378
_alter_files(working_tree, target_tree, tt, child_pb,
1383
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1385
raw_conflicts = resolve_conflicts(tt, child_pb)
1388
conflicts = cook_conflicts(raw_conflicts, tt)
1390
change_reporter = delta.ChangeReporter(
1391
unversioned_filter=working_tree.is_ignored)
1392
delta.report_changes(tt._iter_changes(), change_reporter)
1393
for conflict in conflicts:
1397
working_tree.set_merge_modified({})
1399
target_tree.unlock()
1405
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1407
merge_modified = working_tree.merge_modified()
1408
change_list = target_tree._iter_changes(working_tree,
1409
specific_files=specific_files, pb=pb)
1410
if target_tree.inventory.root is None:
1416
for id_num, (file_id, path, changed_content, versioned, parent, name,
1417
kind, executable) in enumerate(change_list):
1418
if skip_root and file_id[0] is not None and parent[0] is None:
1420
trans_id = tt.trans_id_file_id(file_id)
1423
keep_content = False
1424
if kind[0] == 'file' and (backups or kind[1] is None):
1425
wt_sha1 = working_tree.get_file_sha1(file_id)
1426
if merge_modified.get(file_id) != wt_sha1:
1427
# acquire the basis tree lazyily to prevent the expense
1428
# of accessing it when its not needed ? (Guessing, RBC,
1430
if basis_tree is None:
1431
basis_tree = working_tree.basis_tree()
1432
basis_tree.lock_read()
1433
if file_id in basis_tree:
1434
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1436
elif kind[1] is None and not versioned[1]:
1438
if kind[0] is not None:
1439
if not keep_content:
1440
tt.delete_contents(trans_id)
1441
elif kind[1] is not None:
1442
parent_trans_id = tt.trans_id_file_id(parent[0])
1443
by_parent = tt.by_parent()
1444
backup_name = _get_backup_name(name[0], by_parent,
1445
parent_trans_id, tt)
1446
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1447
new_trans_id = tt.create_path(name[0], parent_trans_id)
1448
if versioned == (True, True):
1449
tt.unversion_file(trans_id)
1450
tt.version_file(file_id, new_trans_id)
1451
# New contents should have the same unix perms as old
1454
trans_id = new_trans_id
1455
if kind[1] == 'directory':
1456
tt.create_directory(trans_id)
1457
elif kind[1] == 'symlink':
1458
tt.create_symlink(target_tree.get_symlink_target(file_id),
1460
elif kind[1] == 'file':
1461
tt.create_file(target_tree.get_file_lines(file_id),
1463
# preserve the execute bit when backing up
1464
if keep_content and executable[0] == executable[1]:
1465
tt.set_executability(executable[1], trans_id)
1467
assert kind[1] is None
1468
if versioned == (False, True):
1469
tt.version_file(file_id, trans_id)
1470
if versioned == (True, False):
1471
tt.unversion_file(trans_id)
1472
if (name[1] is not None and
1473
(name[0] != name[1] or parent[0] != parent[1])):
1475
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1476
if executable[0] != executable[1] and kind[1] == "file":
1477
tt.set_executability(executable[1], trans_id)
1479
if basis_tree is not None:
1483
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1484
"""Make many conflict-resolution attempts, but die if they fail"""
1485
if pass_func is None:
1486
pass_func = conflict_pass
1487
new_conflicts = set()
1490
pb.update('Resolution pass', n+1, 10)
1491
conflicts = tt.find_conflicts()
1492
if len(conflicts) == 0:
1493
return new_conflicts
1494
new_conflicts.update(pass_func(tt, conflicts))
1495
raise MalformedTransform(conflicts=conflicts)
1500
def conflict_pass(tt, conflicts):
1501
"""Resolve some classes of conflicts."""
1502
new_conflicts = set()
1503
for c_type, conflict in ((c[0], c) for c in conflicts):
1504
if c_type == 'duplicate id':
1505
tt.unversion_file(conflict[1])
1506
new_conflicts.add((c_type, 'Unversioned existing file',
1507
conflict[1], conflict[2], ))
1508
elif c_type == 'duplicate':
1509
# files that were renamed take precedence
1510
new_name = tt.final_name(conflict[1])+'.moved'
1511
final_parent = tt.final_parent(conflict[1])
1512
if tt.path_changed(conflict[1]):
1513
tt.adjust_path(new_name, final_parent, conflict[2])
1514
new_conflicts.add((c_type, 'Moved existing file to',
1515
conflict[2], conflict[1]))
1517
tt.adjust_path(new_name, final_parent, conflict[1])
1518
new_conflicts.add((c_type, 'Moved existing file to',
1519
conflict[1], conflict[2]))
1520
elif c_type == 'parent loop':
1521
# break the loop by undoing one of the ops that caused the loop
1523
while not tt.path_changed(cur):
1524
cur = tt.final_parent(cur)
1525
new_conflicts.add((c_type, 'Cancelled move', cur,
1526
tt.final_parent(cur),))
1527
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1529
elif c_type == 'missing parent':
1530
trans_id = conflict[1]
1532
tt.cancel_deletion(trans_id)
1533
new_conflicts.add(('deleting parent', 'Not deleting',
1536
tt.create_directory(trans_id)
1537
new_conflicts.add((c_type, 'Created directory', trans_id))
1538
elif c_type == 'unversioned parent':
1539
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1540
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1541
return new_conflicts
1544
def cook_conflicts(raw_conflicts, tt):
1545
"""Generate a list of cooked conflicts, sorted by file path"""
1546
from bzrlib.conflicts import Conflict
1547
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1548
return sorted(conflict_iter, key=Conflict.sort_key)
1551
def iter_cook_conflicts(raw_conflicts, tt):
1552
from bzrlib.conflicts import Conflict
1554
for conflict in raw_conflicts:
1555
c_type = conflict[0]
1556
action = conflict[1]
1557
modified_path = fp.get_path(conflict[2])
1558
modified_id = tt.final_file_id(conflict[2])
1559
if len(conflict) == 3:
1560
yield Conflict.factory(c_type, action=action, path=modified_path,
1561
file_id=modified_id)
1564
conflicting_path = fp.get_path(conflict[3])
1565
conflicting_id = tt.final_file_id(conflict[3])
1566
yield Conflict.factory(c_type, action=action, path=modified_path,
1567
file_id=modified_id,
1568
conflict_path=conflicting_path,
1569
conflict_file_id=conflicting_id)