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
to_path = final_paths.get_path(to_trans_id)
1025
if from_kind != to_kind:
1027
elif to_kind in ('file' or 'symlink') and (
1028
to_trans_id != from_trans_id or
1029
to_trans_id in self._new_contents):
1031
if (not modified and from_versioned == to_versioned and
1032
from_parent==to_parent and from_name == to_name and
1033
from_executable == to_executable):
1035
results.append((file_id, to_path, modified,
1036
(from_versioned, to_versioned),
1037
(from_parent, to_parent),
1038
(from_name, to_name),
1039
(from_kind, to_kind),
1040
(from_executable, to_executable)))
1041
return iter(sorted(results, key=lambda x:x[1]))
1044
def joinpath(parent, child):
1045
"""Join tree-relative paths, handling the tree root specially"""
1046
if parent is None or parent == "":
1049
return pathjoin(parent, child)
1052
class FinalPaths(object):
1053
"""Make path calculation cheap by memoizing paths.
1055
The underlying tree must not be manipulated between calls, or else
1056
the results will likely be incorrect.
1058
def __init__(self, transform):
1059
object.__init__(self)
1060
self._known_paths = {}
1061
self.transform = transform
1063
def _determine_path(self, trans_id):
1064
if trans_id == self.transform.root:
1066
name = self.transform.final_name(trans_id)
1067
parent_id = self.transform.final_parent(trans_id)
1068
if parent_id == self.transform.root:
1071
return pathjoin(self.get_path(parent_id), name)
1073
def get_path(self, trans_id):
1074
"""Find the final path associated with a trans_id"""
1075
if trans_id not in self._known_paths:
1076
self._known_paths[trans_id] = self._determine_path(trans_id)
1077
return self._known_paths[trans_id]
1079
def topology_sorted_ids(tree):
1080
"""Determine the topological order of the ids in a tree"""
1081
file_ids = list(tree)
1082
file_ids.sort(key=tree.id2path)
1086
def build_tree(tree, wt):
1087
"""Create working tree for a branch, using a TreeTransform.
1089
This function should be used on empty trees, having a tree root at most.
1090
(see merge and revert functionality for working with existing trees)
1092
Existing files are handled like so:
1094
- Existing bzrdirs take precedence over creating new items. They are
1095
created as '%s.diverted' % name.
1096
- Otherwise, if the content on disk matches the content we are building,
1097
it is silently replaced.
1098
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1100
wt.lock_tree_write()
1104
return _build_tree(tree, wt)
1110
def _build_tree(tree, wt):
1111
"""See build_tree."""
1112
if len(wt.inventory) > 1: # more than just a root
1113
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1115
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1116
pp = ProgressPhase("Build phase", 2, top_pb)
1117
# if tree.inventory.root is not None:
1118
# wt.set_root_id(tree.inventory.root.file_id)
1119
tt = TreeTransform(wt)
1123
file_trans_id[wt.get_root_id()] = \
1124
tt.trans_id_tree_file_id(wt.get_root_id())
1125
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1127
for num, (tree_path, entry) in \
1128
enumerate(tree.inventory.iter_entries_by_dir()):
1129
pb.update("Building tree", num, len(tree.inventory))
1130
if entry.parent_id is None:
1133
file_id = entry.file_id
1134
target_path = wt.abspath(tree_path)
1136
kind = file_kind(target_path)
1140
if kind == "directory":
1142
bzrdir.BzrDir.open(target_path)
1143
except errors.NotBranchError:
1147
if (file_id not in divert and
1148
_content_match(tree, entry, file_id, kind,
1150
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1151
if kind == 'directory':
1153
if entry.parent_id not in file_trans_id:
1154
raise repr(entry.parent_id)
1155
parent_id = file_trans_id[entry.parent_id]
1156
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1159
new_trans_id = file_trans_id[file_id]
1160
old_parent = tt.trans_id_tree_path(tree_path)
1161
_reparent_children(tt, old_parent, new_trans_id)
1165
divert_trans = set(file_trans_id[f] for f in divert)
1166
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1167
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1168
conflicts = cook_conflicts(raw_conflicts, tt)
1169
for conflict in conflicts:
1172
wt.add_conflicts(conflicts)
1173
except errors.UnsupportedOperation:
1181
def _reparent_children(tt, old_parent, new_parent):
1182
for child in tt.iter_tree_children(old_parent):
1183
tt.adjust_path(tt.final_name(child), new_parent, child)
1186
def _content_match(tree, entry, file_id, kind, target_path):
1187
if entry.kind != kind:
1189
if entry.kind == "directory":
1191
if entry.kind == "file":
1192
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1194
elif entry.kind == "symlink":
1195
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1200
def resolve_checkout(tt, conflicts, divert):
1201
new_conflicts = set()
1202
for c_type, conflict in ((c[0], c) for c in conflicts):
1203
# Anything but a 'duplicate' would indicate programmer error
1204
assert c_type == 'duplicate', c_type
1205
# Now figure out which is new and which is old
1206
if tt.new_contents(conflict[1]):
1207
new_file = conflict[1]
1208
old_file = conflict[2]
1210
new_file = conflict[2]
1211
old_file = conflict[1]
1213
# We should only get here if the conflict wasn't completely
1215
final_parent = tt.final_parent(old_file)
1216
if new_file in divert:
1217
new_name = tt.final_name(old_file)+'.diverted'
1218
tt.adjust_path(new_name, final_parent, new_file)
1219
new_conflicts.add((c_type, 'Diverted to',
1220
new_file, old_file))
1222
new_name = tt.final_name(old_file)+'.moved'
1223
tt.adjust_path(new_name, final_parent, old_file)
1224
new_conflicts.add((c_type, 'Moved existing file to',
1225
old_file, new_file))
1226
return new_conflicts
1229
def new_by_entry(tt, entry, parent_id, tree):
1230
"""Create a new file according to its inventory entry"""
1234
contents = tree.get_file(entry.file_id).readlines()
1235
executable = tree.is_executable(entry.file_id)
1236
return tt.new_file(name, parent_id, contents, entry.file_id,
1238
elif kind == 'directory':
1239
return tt.new_directory(name, parent_id, entry.file_id)
1240
elif kind == 'symlink':
1241
target = tree.get_symlink_target(entry.file_id)
1242
return tt.new_symlink(name, parent_id, target, entry.file_id)
1244
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1245
"""Create new file contents according to an inventory entry."""
1246
if entry.kind == "file":
1248
lines = tree.get_file(entry.file_id).readlines()
1249
tt.create_file(lines, trans_id, mode_id=mode_id)
1250
elif entry.kind == "symlink":
1251
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1252
elif entry.kind == "directory":
1253
tt.create_directory(trans_id)
1255
def create_entry_executability(tt, entry, trans_id):
1256
"""Set the executability of a trans_id according to an inventory entry"""
1257
if entry.kind == "file":
1258
tt.set_executability(entry.executable, trans_id)
1261
@deprecated_function(zero_fifteen)
1262
def find_interesting(working_tree, target_tree, filenames):
1263
"""Find the ids corresponding to specified filenames.
1265
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1267
working_tree.lock_read()
1269
target_tree.lock_read()
1271
return working_tree.paths2ids(filenames, [target_tree])
1273
target_tree.unlock()
1275
working_tree.unlock()
1278
def change_entry(tt, file_id, working_tree, target_tree,
1279
trans_id_file_id, backups, trans_id, by_parent):
1280
"""Replace a file_id's contents with those from a target tree."""
1281
e_trans_id = trans_id_file_id(file_id)
1282
entry = target_tree.inventory[file_id]
1283
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1286
mode_id = e_trans_id
1289
tt.delete_contents(e_trans_id)
1291
parent_trans_id = trans_id_file_id(entry.parent_id)
1292
backup_name = get_backup_name(entry, by_parent,
1293
parent_trans_id, tt)
1294
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1295
tt.unversion_file(e_trans_id)
1296
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1297
tt.version_file(file_id, e_trans_id)
1298
trans_id[file_id] = e_trans_id
1299
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1300
create_entry_executability(tt, entry, e_trans_id)
1303
tt.set_executability(entry.executable, e_trans_id)
1304
if tt.final_name(e_trans_id) != entry.name:
1307
parent_id = tt.final_parent(e_trans_id)
1308
parent_file_id = tt.final_file_id(parent_id)
1309
if parent_file_id != entry.parent_id:
1314
parent_trans_id = trans_id_file_id(entry.parent_id)
1315
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1318
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1319
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1322
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1323
"""Produce a backup-style name that appears to be available"""
1327
yield "%s.~%d~" % (name, counter)
1329
for new_name in name_gen():
1330
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1334
def _entry_changes(file_id, entry, working_tree):
1335
"""Determine in which ways the inventory entry has changed.
1337
Returns booleans: has_contents, content_mod, meta_mod
1338
has_contents means there are currently contents, but they differ
1339
contents_mod means contents need to be modified
1340
meta_mod means the metadata needs to be modified
1342
cur_entry = working_tree.inventory[file_id]
1344
working_kind = working_tree.kind(file_id)
1347
has_contents = False
1350
if has_contents is True:
1351
if entry.kind != working_kind:
1352
contents_mod, meta_mod = True, False
1354
cur_entry._read_tree_state(working_tree.id2path(file_id),
1356
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1357
cur_entry._forget_tree_state()
1358
return has_contents, contents_mod, meta_mod
1361
def revert(working_tree, target_tree, filenames, backups=False,
1362
pb=DummyProgress(), change_reporter=None):
1363
"""Revert a working tree's contents to those of a target tree."""
1364
target_tree.lock_read()
1365
tt = TreeTransform(working_tree, pb)
1367
pp = ProgressPhase("Revert phase", 3, pb)
1369
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1371
_alter_files(working_tree, target_tree, tt, child_pb,
1376
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1378
raw_conflicts = resolve_conflicts(tt, child_pb)
1381
conflicts = cook_conflicts(raw_conflicts, tt)
1383
change_reporter = delta.ChangeReporter(working_tree.inventory)
1384
delta.report_changes(tt._iter_changes(), change_reporter)
1385
for conflict in conflicts:
1389
working_tree.set_merge_modified({})
1391
target_tree.unlock()
1397
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1399
merge_modified = working_tree.merge_modified()
1400
change_list = target_tree._iter_changes(working_tree,
1401
specific_files=specific_files, pb=pb)
1402
if target_tree.inventory.root is None:
1408
for id_num, (file_id, path, changed_content, versioned, parent, name,
1409
kind, executable) in enumerate(change_list):
1410
if skip_root and file_id[0] is not None and parent[0] is None:
1412
trans_id = tt.trans_id_file_id(file_id)
1415
keep_content = False
1416
if kind[0] == 'file' and (backups or kind[1] is None):
1417
wt_sha1 = working_tree.get_file_sha1(file_id)
1418
if merge_modified.get(file_id) != wt_sha1:
1419
# acquire the basis tree lazyily to prevent the expense
1420
# of accessing it when its not needed ? (Guessing, RBC,
1422
if basis_tree is None:
1423
basis_tree = working_tree.basis_tree()
1424
basis_tree.lock_read()
1425
if file_id in basis_tree:
1426
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1428
elif kind[1] is None and not versioned[1]:
1430
if kind[0] is not None:
1431
if not keep_content:
1432
tt.delete_contents(trans_id)
1433
elif kind[1] is not None:
1434
parent_trans_id = tt.trans_id_file_id(parent[0])
1435
by_parent = tt.by_parent()
1436
backup_name = _get_backup_name(name[0], by_parent,
1437
parent_trans_id, tt)
1438
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1439
new_trans_id = tt.create_path(name[0], parent_trans_id)
1440
if versioned == (True, True):
1441
tt.unversion_file(trans_id)
1442
tt.version_file(file_id, new_trans_id)
1443
# New contents should have the same unix perms as old
1446
trans_id = new_trans_id
1447
if kind[1] == 'directory':
1448
tt.create_directory(trans_id)
1449
elif kind[1] == 'symlink':
1450
tt.create_symlink(target_tree.get_symlink_target(file_id),
1452
elif kind[1] == 'file':
1453
tt.create_file(target_tree.get_file_lines(file_id),
1455
# preserve the execute bit when backing up
1456
if keep_content and executable[0] == executable[1]:
1457
tt.set_executability(executable[1], trans_id)
1459
assert kind[1] is None
1460
if versioned == (False, True):
1461
tt.version_file(file_id, trans_id)
1462
if versioned == (True, False):
1463
tt.unversion_file(trans_id)
1464
if (name[1] is not None and
1465
(name[0] != name[1] or parent[0] != parent[1])):
1467
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1468
if executable[0] != executable[1] and kind[1] == "file":
1469
tt.set_executability(executable[1], trans_id)
1471
if basis_tree is not None:
1475
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1476
"""Make many conflict-resolution attempts, but die if they fail"""
1477
if pass_func is None:
1478
pass_func = conflict_pass
1479
new_conflicts = set()
1482
pb.update('Resolution pass', n+1, 10)
1483
conflicts = tt.find_conflicts()
1484
if len(conflicts) == 0:
1485
return new_conflicts
1486
new_conflicts.update(pass_func(tt, conflicts))
1487
raise MalformedTransform(conflicts=conflicts)
1492
def conflict_pass(tt, conflicts):
1493
"""Resolve some classes of conflicts."""
1494
new_conflicts = set()
1495
for c_type, conflict in ((c[0], c) for c in conflicts):
1496
if c_type == 'duplicate id':
1497
tt.unversion_file(conflict[1])
1498
new_conflicts.add((c_type, 'Unversioned existing file',
1499
conflict[1], conflict[2], ))
1500
elif c_type == 'duplicate':
1501
# files that were renamed take precedence
1502
new_name = tt.final_name(conflict[1])+'.moved'
1503
final_parent = tt.final_parent(conflict[1])
1504
if tt.path_changed(conflict[1]):
1505
tt.adjust_path(new_name, final_parent, conflict[2])
1506
new_conflicts.add((c_type, 'Moved existing file to',
1507
conflict[2], conflict[1]))
1509
tt.adjust_path(new_name, final_parent, conflict[1])
1510
new_conflicts.add((c_type, 'Moved existing file to',
1511
conflict[1], conflict[2]))
1512
elif c_type == 'parent loop':
1513
# break the loop by undoing one of the ops that caused the loop
1515
while not tt.path_changed(cur):
1516
cur = tt.final_parent(cur)
1517
new_conflicts.add((c_type, 'Cancelled move', cur,
1518
tt.final_parent(cur),))
1519
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1521
elif c_type == 'missing parent':
1522
trans_id = conflict[1]
1524
tt.cancel_deletion(trans_id)
1525
new_conflicts.add(('deleting parent', 'Not deleting',
1528
tt.create_directory(trans_id)
1529
new_conflicts.add((c_type, 'Created directory', trans_id))
1530
elif c_type == 'unversioned parent':
1531
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1532
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1533
return new_conflicts
1536
def cook_conflicts(raw_conflicts, tt):
1537
"""Generate a list of cooked conflicts, sorted by file path"""
1538
from bzrlib.conflicts import Conflict
1539
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1540
return sorted(conflict_iter, key=Conflict.sort_key)
1543
def iter_cook_conflicts(raw_conflicts, tt):
1544
from bzrlib.conflicts import Conflict
1546
for conflict in raw_conflicts:
1547
c_type = conflict[0]
1548
action = conflict[1]
1549
modified_path = fp.get_path(conflict[2])
1550
modified_id = tt.final_file_id(conflict[2])
1551
if len(conflict) == 3:
1552
yield Conflict.factory(c_type, action=action, path=modified_path,
1553
file_id=modified_id)
1556
conflicting_path = fp.get_path(conflict[3])
1557
conflicting_id = tt.final_file_id(conflict[3])
1558
yield Conflict.factory(c_type, action=action, path=modified_path,
1559
file_id=modified_id,
1560
conflict_path=conflicting_path,
1561
conflict_file_id=conflicting_id)