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.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
23
ReusingTransform, NotVersionedError, CantMoveRoot,
24
ExistingLimbo, ImmortalLimbo)
25
from bzrlib.inventory import InventoryEntry
26
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
28
from bzrlib.progress import DummyProgress, ProgressPhase
29
from bzrlib.trace import mutter, warning
30
from bzrlib import tree
32
import bzrlib.urlutils as urlutils
35
ROOT_PARENT = "root-parent"
38
def unique_add(map, key, value):
40
raise DuplicateKey(key=key)
44
class _TransformResults(object):
45
def __init__(self, modified_paths):
47
self.modified_paths = modified_paths
50
class TreeTransform(object):
51
"""Represent a tree transformation.
53
This object is designed to support incremental generation of the transform,
56
It is easy to produce malformed transforms, but they are generally
57
harmless. Attempting to apply a malformed transform will cause an
58
exception to be raised before any modifications are made to the tree.
60
Many kinds of malformed transforms can be corrected with the
61
resolve_conflicts function. The remaining ones indicate programming error,
62
such as trying to create a file with no path.
64
Two sets of file creation methods are supplied. Convenience methods are:
69
These are composed of the low-level methods:
71
* create_file or create_directory or create_symlink
75
def __init__(self, tree, pb=DummyProgress()):
76
"""Note: a tree_write lock is taken on the tree.
78
Use TreeTransform.finalize() to release the lock
82
self._tree.lock_tree_write()
84
control_files = self._tree._control_files
85
self._limbodir = urlutils.local_path_from_url(
86
control_files.controlfilename('limbo'))
88
os.mkdir(self._limbodir)
90
if e.errno == errno.EEXIST:
91
raise ExistingLimbo(self._limbodir)
99
self._new_contents = {}
100
self._removed_contents = set()
101
self._new_executability = {}
103
self._non_present_ids = {}
105
self._removed_id = set()
106
self._tree_path_ids = {}
107
self._tree_id_paths = {}
109
# Cache of realpath results, to speed up canonical_path
111
# Cache of relpath results, to speed up canonical_path
112
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
116
def __get_root(self):
117
return self._new_root
119
root = property(__get_root)
122
"""Release the working tree lock, if held, clean up limbo dir."""
123
if self._tree is None:
126
for trans_id, kind in self._new_contents.iteritems():
127
path = self._limbo_name(trans_id)
128
if kind == "directory":
133
os.rmdir(self._limbodir)
135
# We don't especially care *why* the dir is immortal.
136
raise ImmortalLimbo(self._limbodir)
141
def _assign_id(self):
142
"""Produce a new tranform id"""
143
new_id = "new-%s" % self._id_number
147
def create_path(self, name, parent):
148
"""Assign a transaction id to a new path"""
149
trans_id = self._assign_id()
150
unique_add(self._new_name, trans_id, name)
151
unique_add(self._new_parent, trans_id, parent)
154
def adjust_path(self, name, parent, trans_id):
155
"""Change the path that is assigned to a transaction id."""
156
if trans_id == self._new_root:
158
self._new_name[trans_id] = name
159
self._new_parent[trans_id] = parent
161
def adjust_root_path(self, name, parent):
162
"""Emulate moving the root by moving all children, instead.
164
We do this by undoing the association of root's transaction id with the
165
current tree. This allows us to create a new directory with that
166
transaction id. We unversion the root directory and version the
167
physically new directory, and hope someone versions the tree root
170
old_root = self._new_root
171
old_root_file_id = self.final_file_id(old_root)
172
# force moving all children of root
173
for child_id in self.iter_tree_children(old_root):
174
if child_id != parent:
175
self.adjust_path(self.final_name(child_id),
176
self.final_parent(child_id), child_id)
177
file_id = self.final_file_id(child_id)
178
if file_id is not None:
179
self.unversion_file(child_id)
180
self.version_file(file_id, child_id)
182
# the physical root needs a new transaction id
183
self._tree_path_ids.pop("")
184
self._tree_id_paths.pop(old_root)
185
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
186
if parent == old_root:
187
parent = self._new_root
188
self.adjust_path(name, parent, old_root)
189
self.create_directory(old_root)
190
self.version_file(old_root_file_id, old_root)
191
self.unversion_file(self._new_root)
193
def trans_id_tree_file_id(self, inventory_id):
194
"""Determine the transaction id of a working tree file.
196
This reflects only files that already exist, not ones that will be
197
added by transactions.
199
path = self._tree.inventory.id2path(inventory_id)
200
return self.trans_id_tree_path(path)
202
def trans_id_file_id(self, file_id):
203
"""Determine or set the transaction id associated with a file ID.
204
A new id is only created for file_ids that were never present. If
205
a transaction has been unversioned, it is deliberately still returned.
206
(this will likely lead to an unversioned parent conflict.)
208
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
209
return self._r_new_id[file_id]
210
elif file_id in self._tree.inventory:
211
return self.trans_id_tree_file_id(file_id)
212
elif file_id in self._non_present_ids:
213
return self._non_present_ids[file_id]
215
trans_id = self._assign_id()
216
self._non_present_ids[file_id] = trans_id
219
def canonical_path(self, path):
220
"""Get the canonical tree-relative path"""
221
# don't follow final symlinks
222
abs = self._tree.abspath(path)
223
if abs in self._relpaths:
224
return self._relpaths[abs]
225
dirname, basename = os.path.split(abs)
226
if dirname not in self._realpaths:
227
self._realpaths[dirname] = os.path.realpath(dirname)
228
dirname = self._realpaths[dirname]
229
abs = pathjoin(dirname, basename)
230
if dirname in self._relpaths:
231
relpath = pathjoin(self._relpaths[dirname], basename)
232
relpath = relpath.rstrip('/\\')
234
relpath = self._tree.relpath(abs)
235
self._relpaths[abs] = relpath
238
def trans_id_tree_path(self, path):
239
"""Determine (and maybe set) the transaction ID for a tree path."""
240
path = self.canonical_path(path)
241
if path not in self._tree_path_ids:
242
self._tree_path_ids[path] = self._assign_id()
243
self._tree_id_paths[self._tree_path_ids[path]] = path
244
return self._tree_path_ids[path]
246
def get_tree_parent(self, trans_id):
247
"""Determine id of the parent in the tree."""
248
path = self._tree_id_paths[trans_id]
251
return self.trans_id_tree_path(os.path.dirname(path))
253
def create_file(self, contents, trans_id, mode_id=None):
254
"""Schedule creation of a new file.
258
Contents is an iterator of strings, all of which will be written
259
to the target destination.
261
New file takes the permissions of any existing file with that id,
262
unless mode_id is specified.
264
name = self._limbo_name(trans_id)
268
unique_add(self._new_contents, trans_id, 'file')
270
# Clean up the file, it never got registered so
271
# TreeTransform.finalize() won't clean it up.
276
for segment in contents:
280
self._set_mode(trans_id, mode_id, S_ISREG)
282
def _set_mode(self, trans_id, mode_id, typefunc):
283
"""Set the mode of new file contents.
284
The mode_id is the existing file to get the mode from (often the same
285
as trans_id). The operation is only performed if there's a mode match
286
according to typefunc.
291
old_path = self._tree_id_paths[mode_id]
295
mode = os.stat(old_path).st_mode
297
if e.errno == errno.ENOENT:
302
os.chmod(self._limbo_name(trans_id), mode)
304
def create_directory(self, trans_id):
305
"""Schedule creation of a new directory.
307
See also new_directory.
309
os.mkdir(self._limbo_name(trans_id))
310
unique_add(self._new_contents, trans_id, 'directory')
312
def create_symlink(self, target, trans_id):
313
"""Schedule creation of a new symbolic link.
315
target is a bytestring.
316
See also new_symlink.
318
os.symlink(target, self._limbo_name(trans_id))
319
unique_add(self._new_contents, trans_id, 'symlink')
321
def cancel_creation(self, trans_id):
322
"""Cancel the creation of new file contents."""
323
del self._new_contents[trans_id]
324
delete_any(self._limbo_name(trans_id))
326
def delete_contents(self, trans_id):
327
"""Schedule the contents of a path entry for deletion"""
328
self.tree_kind(trans_id)
329
self._removed_contents.add(trans_id)
331
def cancel_deletion(self, trans_id):
332
"""Cancel a scheduled deletion"""
333
self._removed_contents.remove(trans_id)
335
def unversion_file(self, trans_id):
336
"""Schedule a path entry to become unversioned"""
337
self._removed_id.add(trans_id)
339
def delete_versioned(self, trans_id):
340
"""Delete and unversion a versioned file"""
341
self.delete_contents(trans_id)
342
self.unversion_file(trans_id)
344
def set_executability(self, executability, trans_id):
345
"""Schedule setting of the 'execute' bit
346
To unschedule, set to None
348
if executability is None:
349
del self._new_executability[trans_id]
351
unique_add(self._new_executability, trans_id, executability)
353
def version_file(self, file_id, trans_id):
354
"""Schedule a file to become versioned."""
355
assert file_id is not None
356
unique_add(self._new_id, trans_id, file_id)
357
unique_add(self._r_new_id, file_id, trans_id)
359
def cancel_versioning(self, trans_id):
360
"""Undo a previous versioning of a file"""
361
file_id = self._new_id[trans_id]
362
del self._new_id[trans_id]
363
del self._r_new_id[file_id]
366
"""Determine the paths of all new and changed files"""
368
fp = FinalPaths(self)
369
for id_set in (self._new_name, self._new_parent, self._new_contents,
370
self._new_id, self._new_executability):
371
new_ids.update(id_set)
372
new_paths = [(fp.get_path(t), t) for t in new_ids]
376
def tree_kind(self, trans_id):
377
"""Determine the file kind in the working tree.
379
Raises NoSuchFile if the file does not exist
381
path = self._tree_id_paths.get(trans_id)
383
raise NoSuchFile(None)
385
return file_kind(self._tree.abspath(path))
387
if e.errno != errno.ENOENT:
390
raise NoSuchFile(path)
392
def final_kind(self, trans_id):
393
"""Determine the final file kind, after any changes applied.
395
Raises NoSuchFile if the file does not exist/has no contents.
396
(It is conceivable that a path would be created without the
397
corresponding contents insertion command)
399
if trans_id in self._new_contents:
400
return self._new_contents[trans_id]
401
elif trans_id in self._removed_contents:
402
raise NoSuchFile(None)
404
return self.tree_kind(trans_id)
406
def tree_file_id(self, trans_id):
407
"""Determine the file id associated with the trans_id in the tree"""
409
path = self._tree_id_paths[trans_id]
411
# the file is a new, unversioned file, or invalid trans_id
413
# the file is old; the old id is still valid
414
if self._new_root == trans_id:
415
return self._tree.inventory.root.file_id
416
return self._tree.inventory.path2id(path)
418
def final_file_id(self, trans_id):
419
"""Determine the file id after any changes are applied, or None.
421
None indicates that the file will not be versioned after changes are
425
# there is a new id for this file
426
assert self._new_id[trans_id] is not None
427
return self._new_id[trans_id]
429
if trans_id in self._removed_id:
431
return self.tree_file_id(trans_id)
433
def inactive_file_id(self, trans_id):
434
"""Return the inactive file_id associated with a transaction id.
435
That is, the one in the tree or in non_present_ids.
436
The file_id may actually be active, too.
438
file_id = self.tree_file_id(trans_id)
439
if file_id is not None:
441
for key, value in self._non_present_ids.iteritems():
442
if value == trans_id:
445
def final_parent(self, trans_id):
446
"""Determine the parent file_id, after any changes are applied.
448
ROOT_PARENT is returned for the tree root.
451
return self._new_parent[trans_id]
453
return self.get_tree_parent(trans_id)
455
def final_name(self, trans_id):
456
"""Determine the final filename, after all changes are applied."""
458
return self._new_name[trans_id]
460
return os.path.basename(self._tree_id_paths[trans_id])
463
"""Return a map of parent: children for known parents.
465
Only new paths and parents of tree files with assigned ids are used.
468
items = list(self._new_parent.iteritems())
469
items.extend((t, self.final_parent(t)) for t in
470
self._tree_id_paths.keys())
471
for trans_id, parent_id in items:
472
if parent_id not in by_parent:
473
by_parent[parent_id] = set()
474
by_parent[parent_id].add(trans_id)
477
def path_changed(self, trans_id):
478
"""Return True if a trans_id's path has changed."""
479
return (trans_id in self._new_name) or (trans_id in self._new_parent)
481
def new_contents(self, trans_id):
482
return (trans_id in self._new_contents)
484
def find_conflicts(self):
485
"""Find any violations of inventory or filesystem invariants"""
486
if self.__done is True:
487
raise ReusingTransform()
489
# ensure all children of all existent parents are known
490
# all children of non-existent parents are known, by definition.
491
self._add_tree_children()
492
by_parent = self.by_parent()
493
conflicts.extend(self._unversioned_parents(by_parent))
494
conflicts.extend(self._parent_loops())
495
conflicts.extend(self._duplicate_entries(by_parent))
496
conflicts.extend(self._duplicate_ids())
497
conflicts.extend(self._parent_type_conflicts(by_parent))
498
conflicts.extend(self._improper_versioning())
499
conflicts.extend(self._executability_conflicts())
500
conflicts.extend(self._overwrite_conflicts())
503
def _add_tree_children(self):
504
"""Add all the children of all active parents to the known paths.
506
Active parents are those which gain children, and those which are
507
removed. This is a necessary first step in detecting conflicts.
509
parents = self.by_parent().keys()
510
parents.extend([t for t in self._removed_contents if
511
self.tree_kind(t) == 'directory'])
512
for trans_id in self._removed_id:
513
file_id = self.tree_file_id(trans_id)
514
if self._tree.inventory[file_id].kind == 'directory':
515
parents.append(trans_id)
517
for parent_id in parents:
518
# ensure that all children are registered with the transaction
519
list(self.iter_tree_children(parent_id))
521
def iter_tree_children(self, parent_id):
522
"""Iterate through the entry's tree children, if any"""
524
path = self._tree_id_paths[parent_id]
528
children = os.listdir(self._tree.abspath(path))
530
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
534
for child in children:
535
childpath = joinpath(path, child)
536
if self._tree.is_control_filename(childpath):
538
yield self.trans_id_tree_path(childpath)
540
def has_named_child(self, by_parent, parent_id, name):
542
children = by_parent[parent_id]
545
for child in children:
546
if self.final_name(child) == name:
549
path = self._tree_id_paths[parent_id]
552
childpath = joinpath(path, name)
553
child_id = self._tree_path_ids.get(childpath)
555
return lexists(self._tree.abspath(childpath))
557
if self.final_parent(child_id) != parent_id:
559
if child_id in self._removed_contents:
560
# XXX What about dangling file-ids?
565
def _parent_loops(self):
566
"""No entry should be its own ancestor"""
568
for trans_id in self._new_parent:
571
while parent_id is not ROOT_PARENT:
573
parent_id = self.final_parent(parent_id)
574
if parent_id == trans_id:
575
conflicts.append(('parent loop', trans_id))
576
if parent_id in seen:
580
def _unversioned_parents(self, by_parent):
581
"""If parent directories are versioned, children must be versioned."""
583
for parent_id, children in by_parent.iteritems():
584
if parent_id is ROOT_PARENT:
586
if self.final_file_id(parent_id) is not None:
588
for child_id in children:
589
if self.final_file_id(child_id) is not None:
590
conflicts.append(('unversioned parent', parent_id))
594
def _improper_versioning(self):
595
"""Cannot version a file with no contents, or a bad type.
597
However, existing entries with no contents are okay.
600
for trans_id in self._new_id.iterkeys():
602
kind = self.final_kind(trans_id)
604
conflicts.append(('versioning no contents', trans_id))
606
if not InventoryEntry.versionable_kind(kind):
607
conflicts.append(('versioning bad kind', trans_id, kind))
610
def _executability_conflicts(self):
611
"""Check for bad executability changes.
613
Only versioned files may have their executability set, because
614
1. only versioned entries can have executability under windows
615
2. only files can be executable. (The execute bit on a directory
616
does not indicate searchability)
619
for trans_id in self._new_executability:
620
if self.final_file_id(trans_id) is None:
621
conflicts.append(('unversioned executability', trans_id))
624
non_file = self.final_kind(trans_id) != "file"
628
conflicts.append(('non-file executability', trans_id))
631
def _overwrite_conflicts(self):
632
"""Check for overwrites (not permitted on Win32)"""
634
for trans_id in self._new_contents:
636
self.tree_kind(trans_id)
639
if trans_id not in self._removed_contents:
640
conflicts.append(('overwrite', trans_id,
641
self.final_name(trans_id)))
644
def _duplicate_entries(self, by_parent):
645
"""No directory may have two entries with the same name."""
647
for children in by_parent.itervalues():
648
name_ids = [(self.final_name(t), t) for t in children]
652
for name, trans_id in name_ids:
654
kind = self.final_kind(trans_id)
657
file_id = self.final_file_id(trans_id)
658
if kind is None and file_id is None:
660
if name == last_name:
661
conflicts.append(('duplicate', last_trans_id, trans_id,
664
last_trans_id = trans_id
667
def _duplicate_ids(self):
668
"""Each inventory id may only be used once"""
670
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
672
active_tree_ids = set((f for f in self._tree.inventory if
673
f not in removed_tree_ids))
674
for trans_id, file_id in self._new_id.iteritems():
675
if file_id in active_tree_ids:
676
old_trans_id = self.trans_id_tree_file_id(file_id)
677
conflicts.append(('duplicate id', old_trans_id, trans_id))
680
def _parent_type_conflicts(self, by_parent):
681
"""parents must have directory 'contents'."""
683
for parent_id, children in by_parent.iteritems():
684
if parent_id is ROOT_PARENT:
686
if not self._any_contents(children):
688
for child in children:
690
self.final_kind(child)
694
kind = self.final_kind(parent_id)
698
conflicts.append(('missing parent', parent_id))
699
elif kind != "directory":
700
conflicts.append(('non-directory parent', parent_id))
703
def _any_contents(self, trans_ids):
704
"""Return true if any of the trans_ids, will have contents."""
705
for trans_id in trans_ids:
707
kind = self.final_kind(trans_id)
714
"""Apply all changes to the inventory and filesystem.
716
If filesystem or inventory conflicts are present, MalformedTransform
719
conflicts = self.find_conflicts()
720
if len(conflicts) != 0:
721
raise MalformedTransform(conflicts=conflicts)
723
inv = self._tree.inventory
724
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
726
child_pb.update('Apply phase', 0, 2)
727
self._apply_removals(inv, limbo_inv)
728
child_pb.update('Apply phase', 1, 2)
729
modified_paths = self._apply_insertions(inv, limbo_inv)
732
self._tree._write_inventory(inv)
735
return _TransformResults(modified_paths)
737
def _limbo_name(self, trans_id):
738
"""Generate the limbo name of a file"""
739
return pathjoin(self._limbodir, trans_id)
741
def _apply_removals(self, inv, limbo_inv):
742
"""Perform tree operations that remove directory/inventory names.
744
That is, delete files that are to be deleted, and put any files that
745
need renaming into limbo. This must be done in strict child-to-parent
748
tree_paths = list(self._tree_path_ids.iteritems())
749
tree_paths.sort(reverse=True)
750
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
752
for num, data in enumerate(tree_paths):
753
path, trans_id = data
754
child_pb.update('removing file', num, len(tree_paths))
755
full_path = self._tree.abspath(path)
756
if trans_id in self._removed_contents:
757
delete_any(full_path)
758
elif trans_id in self._new_name or trans_id in \
761
os.rename(full_path, self._limbo_name(trans_id))
763
if e.errno != errno.ENOENT:
765
if trans_id in self._removed_id:
766
if trans_id == self._new_root:
767
file_id = self._tree.inventory.root.file_id
769
file_id = self.tree_file_id(trans_id)
771
elif trans_id in self._new_name or trans_id in self._new_parent:
772
file_id = self.tree_file_id(trans_id)
773
if file_id is not None:
774
limbo_inv[trans_id] = inv[file_id]
779
def _apply_insertions(self, inv, limbo_inv):
780
"""Perform tree operations that insert directory/inventory names.
782
That is, create any files that need to be created, and restore from
783
limbo any files that needed renaming. This must be done in strict
784
parent-to-child order.
786
new_paths = self.new_paths()
788
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
790
for num, (path, trans_id) in enumerate(new_paths):
791
child_pb.update('adding file', num, len(new_paths))
793
kind = self._new_contents[trans_id]
795
kind = contents = None
796
if trans_id in self._new_contents or \
797
self.path_changed(trans_id):
798
full_path = self._tree.abspath(path)
800
os.rename(self._limbo_name(trans_id), full_path)
802
# We may be renaming a dangling inventory id
803
if e.errno != errno.ENOENT:
805
if trans_id in self._new_contents:
806
modified_paths.append(full_path)
807
del self._new_contents[trans_id]
809
if trans_id in self._new_id:
811
kind = file_kind(self._tree.abspath(path))
812
inv.add_path(path, kind, self._new_id[trans_id])
813
elif trans_id in self._new_name or trans_id in\
815
entry = limbo_inv.get(trans_id)
816
if entry is not None:
817
entry.name = self.final_name(trans_id)
818
parent_path = os.path.dirname(path)
820
self._tree.inventory.path2id(parent_path)
823
# requires files and inventory entries to be in place
824
if trans_id in self._new_executability:
825
self._set_executability(path, inv, trans_id)
828
return modified_paths
830
def _set_executability(self, path, inv, trans_id):
831
"""Set the executability of versioned files """
832
file_id = inv.path2id(path)
833
new_executability = self._new_executability[trans_id]
834
inv[file_id].executable = new_executability
835
if supports_executable():
836
abspath = self._tree.abspath(path)
837
current_mode = os.stat(abspath).st_mode
838
if new_executability:
841
to_mode = current_mode | (0100 & ~umask)
842
# Enable x-bit for others only if they can read it.
843
if current_mode & 0004:
844
to_mode |= 0001 & ~umask
845
if current_mode & 0040:
846
to_mode |= 0010 & ~umask
848
to_mode = current_mode & ~0111
849
os.chmod(abspath, to_mode)
851
def _new_entry(self, name, parent_id, file_id):
852
"""Helper function to create a new filesystem entry."""
853
trans_id = self.create_path(name, parent_id)
854
if file_id is not None:
855
self.version_file(file_id, trans_id)
858
def new_file(self, name, parent_id, contents, file_id=None,
860
"""Convenience method to create files.
862
name is the name of the file to create.
863
parent_id is the transaction id of the parent directory of the file.
864
contents is an iterator of bytestrings, which will be used to produce
866
:param file_id: The inventory ID of the file, if it is to be versioned.
867
:param executable: Only valid when a file_id has been supplied.
869
trans_id = self._new_entry(name, parent_id, file_id)
870
# TODO: rather than scheduling a set_executable call,
871
# have create_file create the file with the right mode.
872
self.create_file(contents, trans_id)
873
if executable is not None:
874
self.set_executability(executable, trans_id)
877
def new_directory(self, name, parent_id, file_id=None):
878
"""Convenience method to create directories.
880
name is the name of the directory to create.
881
parent_id is the transaction id of the parent directory of the
883
file_id is the inventory ID of the directory, if it is to be versioned.
885
trans_id = self._new_entry(name, parent_id, file_id)
886
self.create_directory(trans_id)
889
def new_symlink(self, name, parent_id, target, file_id=None):
890
"""Convenience method to create symbolic link.
892
name is the name of the symlink to create.
893
parent_id is the transaction id of the parent directory of the symlink.
894
target is a bytestring of the target of the symlink.
895
file_id is the inventory ID of the file, if it is to be versioned.
897
trans_id = self._new_entry(name, parent_id, file_id)
898
self.create_symlink(target, trans_id)
901
def joinpath(parent, child):
902
"""Join tree-relative paths, handling the tree root specially"""
903
if parent is None or parent == "":
906
return pathjoin(parent, child)
909
class FinalPaths(object):
910
"""Make path calculation cheap by memoizing paths.
912
The underlying tree must not be manipulated between calls, or else
913
the results will likely be incorrect.
915
def __init__(self, transform):
916
object.__init__(self)
917
self._known_paths = {}
918
self.transform = transform
920
def _determine_path(self, trans_id):
921
if trans_id == self.transform.root:
923
name = self.transform.final_name(trans_id)
924
parent_id = self.transform.final_parent(trans_id)
925
if parent_id == self.transform.root:
928
return pathjoin(self.get_path(parent_id), name)
930
def get_path(self, trans_id):
931
"""Find the final path associated with a trans_id"""
932
if trans_id not in self._known_paths:
933
self._known_paths[trans_id] = self._determine_path(trans_id)
934
return self._known_paths[trans_id]
936
def topology_sorted_ids(tree):
937
"""Determine the topological order of the ids in a tree"""
938
file_ids = list(tree)
939
file_ids.sort(key=tree.id2path)
943
def build_tree(tree, wt):
944
"""Create working tree for a branch, using a TreeTransform.
946
This function should be used on empty trees, having a tree root at most.
947
(see merge and revert functionality for working with existing trees)
949
Existing files are handled like so:
951
- Existing bzrdirs take precedence over creating new items. They are
952
created as '%s.diverted' % name.
953
- Otherwise, if the content on disk matches the content we are building,
954
it is silently replaced.
955
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
957
assert 2 > len(wt.inventory)
959
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
960
pp = ProgressPhase("Build phase", 2, top_pb)
961
tt = TreeTransform(wt)
965
file_trans_id[wt.get_root_id()] = \
966
tt.trans_id_tree_file_id(wt.get_root_id())
967
pb = bzrlib.ui.ui_factory.nested_progress_bar()
969
for num, (tree_path, entry) in \
970
enumerate(tree.inventory.iter_entries_by_dir()):
971
pb.update("Building tree", num, len(tree.inventory))
972
if entry.parent_id is None:
975
file_id = entry.file_id
976
target_path = wt.abspath(tree_path)
978
kind = file_kind(target_path)
982
if kind == "directory":
984
bzrdir.BzrDir.open(target_path)
985
except errors.NotBranchError:
989
if (file_id not in divert and
990
_content_match(tree, entry, file_id, kind,
992
tt.delete_contents(tt.trans_id_tree_path(tree_path))
993
if kind == 'directory':
995
if entry.parent_id not in file_trans_id:
996
raise repr(entry.parent_id)
997
parent_id = file_trans_id[entry.parent_id]
998
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1001
new_trans_id = file_trans_id[file_id]
1002
old_parent = tt.trans_id_tree_path(tree_path)
1003
_reparent_children(tt, old_parent, new_trans_id)
1007
divert_trans = set(file_trans_id[f] for f in divert)
1008
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1009
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1010
conflicts = cook_conflicts(raw_conflicts, tt)
1011
for conflict in conflicts:
1014
wt.add_conflicts(conflicts)
1015
except errors.UnsupportedOperation:
1023
def _reparent_children(tt, old_parent, new_parent):
1024
for child in tt.iter_tree_children(old_parent):
1025
tt.adjust_path(tt.final_name(child), new_parent, child)
1028
def _content_match(tree, entry, file_id, kind, target_path):
1029
if entry.kind != kind:
1031
if entry.kind == "directory":
1033
if entry.kind == "file":
1034
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1036
elif entry.kind == "symlink":
1037
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1042
def resolve_checkout(tt, conflicts, divert):
1043
new_conflicts = set()
1044
for c_type, conflict in ((c[0], c) for c in conflicts):
1045
# Anything but a 'duplicate' would indicate programmer error
1046
assert c_type == 'duplicate', c_type
1047
# Now figure out which is new and which is old
1048
if tt.new_contents(conflict[1]):
1049
new_file = conflict[1]
1050
old_file = conflict[2]
1052
new_file = conflict[2]
1053
old_file = conflict[1]
1055
# We should only get here if the conflict wasn't completely
1057
final_parent = tt.final_parent(old_file)
1058
if new_file in divert:
1059
new_name = tt.final_name(old_file)+'.diverted'
1060
tt.adjust_path(new_name, final_parent, new_file)
1061
new_conflicts.add((c_type, 'Diverted to',
1062
new_file, old_file))
1064
new_name = tt.final_name(old_file)+'.moved'
1065
tt.adjust_path(new_name, final_parent, old_file)
1066
new_conflicts.add((c_type, 'Moved existing file to',
1067
old_file, new_file))
1068
return new_conflicts
1071
def new_by_entry(tt, entry, parent_id, tree):
1072
"""Create a new file according to its inventory entry"""
1076
contents = tree.get_file(entry.file_id).readlines()
1077
executable = tree.is_executable(entry.file_id)
1078
return tt.new_file(name, parent_id, contents, entry.file_id,
1080
elif kind == 'directory':
1081
return tt.new_directory(name, parent_id, entry.file_id)
1082
elif kind == 'symlink':
1083
target = tree.get_symlink_target(entry.file_id)
1084
return tt.new_symlink(name, parent_id, target, entry.file_id)
1086
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1087
"""Create new file contents according to an inventory entry."""
1088
if entry.kind == "file":
1090
lines = tree.get_file(entry.file_id).readlines()
1091
tt.create_file(lines, trans_id, mode_id=mode_id)
1092
elif entry.kind == "symlink":
1093
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1094
elif entry.kind == "directory":
1095
tt.create_directory(trans_id)
1097
def create_entry_executability(tt, entry, trans_id):
1098
"""Set the executability of a trans_id according to an inventory entry"""
1099
if entry.kind == "file":
1100
tt.set_executability(entry.executable, trans_id)
1103
def find_interesting(working_tree, target_tree, filenames):
1104
"""Find the ids corresponding to specified filenames."""
1105
trees = (working_tree, target_tree)
1106
return tree.find_ids_across_trees(filenames, trees)
1109
def change_entry(tt, file_id, working_tree, target_tree,
1110
trans_id_file_id, backups, trans_id, by_parent):
1111
"""Replace a file_id's contents with those from a target tree."""
1112
e_trans_id = trans_id_file_id(file_id)
1113
entry = target_tree.inventory[file_id]
1114
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1117
mode_id = e_trans_id
1120
tt.delete_contents(e_trans_id)
1122
parent_trans_id = trans_id_file_id(entry.parent_id)
1123
backup_name = get_backup_name(entry, by_parent,
1124
parent_trans_id, tt)
1125
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1126
tt.unversion_file(e_trans_id)
1127
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1128
tt.version_file(file_id, e_trans_id)
1129
trans_id[file_id] = e_trans_id
1130
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1131
create_entry_executability(tt, entry, e_trans_id)
1134
tt.set_executability(entry.executable, e_trans_id)
1135
if tt.final_name(e_trans_id) != entry.name:
1138
parent_id = tt.final_parent(e_trans_id)
1139
parent_file_id = tt.final_file_id(parent_id)
1140
if parent_file_id != entry.parent_id:
1145
parent_trans_id = trans_id_file_id(entry.parent_id)
1146
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1149
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1150
"""Produce a backup-style name that appears to be available"""
1154
yield "%s.~%d~" % (entry.name, counter)
1156
for name in name_gen():
1157
if not tt.has_named_child(by_parent, parent_trans_id, name):
1160
def _entry_changes(file_id, entry, working_tree):
1161
"""Determine in which ways the inventory entry has changed.
1163
Returns booleans: has_contents, content_mod, meta_mod
1164
has_contents means there are currently contents, but they differ
1165
contents_mod means contents need to be modified
1166
meta_mod means the metadata needs to be modified
1168
cur_entry = working_tree.inventory[file_id]
1170
working_kind = working_tree.kind(file_id)
1173
has_contents = False
1176
if has_contents is True:
1177
if entry.kind != working_kind:
1178
contents_mod, meta_mod = True, False
1180
cur_entry._read_tree_state(working_tree.id2path(file_id),
1182
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1183
cur_entry._forget_tree_state()
1184
return has_contents, contents_mod, meta_mod
1187
def revert(working_tree, target_tree, filenames, backups=False,
1188
pb=DummyProgress()):
1189
"""Revert a working tree's contents to those of a target tree."""
1190
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1191
def interesting(file_id):
1192
return interesting_ids is None or (file_id in interesting_ids)
1194
tt = TreeTransform(working_tree, pb)
1196
merge_modified = working_tree.merge_modified()
1198
def trans_id_file_id(file_id):
1200
return trans_id[file_id]
1202
return tt.trans_id_tree_file_id(file_id)
1204
pp = ProgressPhase("Revert phase", 4, pb)
1206
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1208
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1210
by_parent = tt.by_parent()
1211
for id_num, file_id in enumerate(sorted_interesting):
1212
child_pb.update("Reverting file", id_num+1,
1213
len(sorted_interesting))
1214
if file_id not in working_tree.inventory:
1215
entry = target_tree.inventory[file_id]
1216
parent_id = trans_id_file_id(entry.parent_id)
1217
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1218
trans_id[file_id] = e_trans_id
1220
backup_this = backups
1221
if file_id in merge_modified:
1223
del merge_modified[file_id]
1224
change_entry(tt, file_id, working_tree, target_tree,
1225
trans_id_file_id, backup_this, trans_id,
1230
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1231
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1233
for id_num, file_id in enumerate(wt_interesting):
1234
child_pb.update("New file check", id_num+1,
1235
len(sorted_interesting))
1236
if file_id not in target_tree:
1237
trans_id = tt.trans_id_tree_file_id(file_id)
1238
tt.unversion_file(trans_id)
1240
file_kind = working_tree.kind(file_id)
1243
if file_kind != 'file' and file_kind is not None:
1244
keep_contents = False
1245
delete_merge_modified = False
1247
if (file_id in merge_modified and
1248
merge_modified[file_id] ==
1249
working_tree.get_file_sha1(file_id)):
1250
keep_contents = False
1251
delete_merge_modified = True
1253
keep_contents = True
1254
delete_merge_modified = False
1255
if not keep_contents:
1256
tt.delete_contents(trans_id)
1257
if delete_merge_modified:
1258
del merge_modified[file_id]
1262
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1264
raw_conflicts = resolve_conflicts(tt, child_pb)
1267
conflicts = cook_conflicts(raw_conflicts, tt)
1268
for conflict in conflicts:
1272
working_tree.set_merge_modified({})
1279
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1280
"""Make many conflict-resolution attempts, but die if they fail"""
1281
if pass_func is None:
1282
pass_func = conflict_pass
1283
new_conflicts = set()
1286
pb.update('Resolution pass', n+1, 10)
1287
conflicts = tt.find_conflicts()
1288
if len(conflicts) == 0:
1289
return new_conflicts
1290
new_conflicts.update(pass_func(tt, conflicts))
1291
raise MalformedTransform(conflicts=conflicts)
1296
def conflict_pass(tt, conflicts):
1297
"""Resolve some classes of conflicts."""
1298
new_conflicts = set()
1299
for c_type, conflict in ((c[0], c) for c in conflicts):
1300
if c_type == 'duplicate id':
1301
tt.unversion_file(conflict[1])
1302
new_conflicts.add((c_type, 'Unversioned existing file',
1303
conflict[1], conflict[2], ))
1304
elif c_type == 'duplicate':
1305
# files that were renamed take precedence
1306
new_name = tt.final_name(conflict[1])+'.moved'
1307
final_parent = tt.final_parent(conflict[1])
1308
if tt.path_changed(conflict[1]):
1309
tt.adjust_path(new_name, final_parent, conflict[2])
1310
new_conflicts.add((c_type, 'Moved existing file to',
1311
conflict[2], conflict[1]))
1313
tt.adjust_path(new_name, final_parent, conflict[1])
1314
new_conflicts.add((c_type, 'Moved existing file to',
1315
conflict[1], conflict[2]))
1316
elif c_type == 'parent loop':
1317
# break the loop by undoing one of the ops that caused the loop
1319
while not tt.path_changed(cur):
1320
cur = tt.final_parent(cur)
1321
new_conflicts.add((c_type, 'Cancelled move', cur,
1322
tt.final_parent(cur),))
1323
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1325
elif c_type == 'missing parent':
1326
trans_id = conflict[1]
1328
tt.cancel_deletion(trans_id)
1329
new_conflicts.add(('deleting parent', 'Not deleting',
1332
tt.create_directory(trans_id)
1333
new_conflicts.add((c_type, 'Created directory', trans_id))
1334
elif c_type == 'unversioned parent':
1335
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1336
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1337
return new_conflicts
1340
def cook_conflicts(raw_conflicts, tt):
1341
"""Generate a list of cooked conflicts, sorted by file path"""
1342
from bzrlib.conflicts import Conflict
1343
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1344
return sorted(conflict_iter, key=Conflict.sort_key)
1347
def iter_cook_conflicts(raw_conflicts, tt):
1348
from bzrlib.conflicts import Conflict
1350
for conflict in raw_conflicts:
1351
c_type = conflict[0]
1352
action = conflict[1]
1353
modified_path = fp.get_path(conflict[2])
1354
modified_id = tt.final_file_id(conflict[2])
1355
if len(conflict) == 3:
1356
yield Conflict.factory(c_type, action=action, path=modified_path,
1357
file_id=modified_id)
1360
conflicting_path = fp.get_path(conflict[3])
1361
conflicting_id = tt.final_file_id(conflict[3])
1362
yield Conflict.factory(c_type, action=action, path=modified_path,
1363
file_id=modified_id,
1364
conflict_path=conflicting_path,
1365
conflict_file_id=conflicting_id)