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, NoFinalPath)
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
f.writelines(contents)
279
self._set_mode(trans_id, mode_id, S_ISREG)
281
def _set_mode(self, trans_id, mode_id, typefunc):
282
"""Set the mode of new file contents.
283
The mode_id is the existing file to get the mode from (often the same
284
as trans_id). The operation is only performed if there's a mode match
285
according to typefunc.
290
old_path = self._tree_id_paths[mode_id]
294
mode = os.stat(self._tree.abspath(old_path)).st_mode
296
if e.errno == errno.ENOENT:
301
os.chmod(self._limbo_name(trans_id), mode)
303
def create_directory(self, trans_id):
304
"""Schedule creation of a new directory.
306
See also new_directory.
308
os.mkdir(self._limbo_name(trans_id))
309
unique_add(self._new_contents, trans_id, 'directory')
311
def create_symlink(self, target, trans_id):
312
"""Schedule creation of a new symbolic link.
314
target is a bytestring.
315
See also new_symlink.
317
os.symlink(target, self._limbo_name(trans_id))
318
unique_add(self._new_contents, trans_id, 'symlink')
320
def cancel_creation(self, trans_id):
321
"""Cancel the creation of new file contents."""
322
del self._new_contents[trans_id]
323
delete_any(self._limbo_name(trans_id))
325
def delete_contents(self, trans_id):
326
"""Schedule the contents of a path entry for deletion"""
327
self.tree_kind(trans_id)
328
self._removed_contents.add(trans_id)
330
def cancel_deletion(self, trans_id):
331
"""Cancel a scheduled deletion"""
332
self._removed_contents.remove(trans_id)
334
def unversion_file(self, trans_id):
335
"""Schedule a path entry to become unversioned"""
336
self._removed_id.add(trans_id)
338
def delete_versioned(self, trans_id):
339
"""Delete and unversion a versioned file"""
340
self.delete_contents(trans_id)
341
self.unversion_file(trans_id)
343
def set_executability(self, executability, trans_id):
344
"""Schedule setting of the 'execute' bit
345
To unschedule, set to None
347
if executability is None:
348
del self._new_executability[trans_id]
350
unique_add(self._new_executability, trans_id, executability)
352
def version_file(self, file_id, trans_id):
353
"""Schedule a file to become versioned."""
354
assert file_id is not None
355
unique_add(self._new_id, trans_id, file_id)
356
unique_add(self._r_new_id, file_id, trans_id)
358
def cancel_versioning(self, trans_id):
359
"""Undo a previous versioning of a file"""
360
file_id = self._new_id[trans_id]
361
del self._new_id[trans_id]
362
del self._r_new_id[file_id]
365
"""Determine the paths of all new and changed files"""
367
fp = FinalPaths(self)
368
for id_set in (self._new_name, self._new_parent, self._new_contents,
369
self._new_id, self._new_executability):
370
new_ids.update(id_set)
371
new_paths = [(fp.get_path(t), t) for t in new_ids]
375
def tree_kind(self, trans_id):
376
"""Determine the file kind in the working tree.
378
Raises NoSuchFile if the file does not exist
380
path = self._tree_id_paths.get(trans_id)
382
raise NoSuchFile(None)
384
return file_kind(self._tree.abspath(path))
386
if e.errno != errno.ENOENT:
389
raise NoSuchFile(path)
391
def final_kind(self, trans_id):
392
"""Determine the final file kind, after any changes applied.
394
Raises NoSuchFile if the file does not exist/has no contents.
395
(It is conceivable that a path would be created without the
396
corresponding contents insertion command)
398
if trans_id in self._new_contents:
399
return self._new_contents[trans_id]
400
elif trans_id in self._removed_contents:
401
raise NoSuchFile(None)
403
return self.tree_kind(trans_id)
405
def tree_file_id(self, trans_id):
406
"""Determine the file id associated with the trans_id in the tree"""
408
path = self._tree_id_paths[trans_id]
410
# the file is a new, unversioned file, or invalid trans_id
412
# the file is old; the old id is still valid
413
if self._new_root == trans_id:
414
return self._tree.inventory.root.file_id
415
return self._tree.inventory.path2id(path)
417
def final_file_id(self, trans_id):
418
"""Determine the file id after any changes are applied, or None.
420
None indicates that the file will not be versioned after changes are
424
# there is a new id for this file
425
assert self._new_id[trans_id] is not None
426
return self._new_id[trans_id]
428
if trans_id in self._removed_id:
430
return self.tree_file_id(trans_id)
432
def inactive_file_id(self, trans_id):
433
"""Return the inactive file_id associated with a transaction id.
434
That is, the one in the tree or in non_present_ids.
435
The file_id may actually be active, too.
437
file_id = self.tree_file_id(trans_id)
438
if file_id is not None:
440
for key, value in self._non_present_ids.iteritems():
441
if value == trans_id:
444
def final_parent(self, trans_id):
445
"""Determine the parent file_id, after any changes are applied.
447
ROOT_PARENT is returned for the tree root.
450
return self._new_parent[trans_id]
452
return self.get_tree_parent(trans_id)
454
def final_name(self, trans_id):
455
"""Determine the final filename, after all changes are applied."""
457
return self._new_name[trans_id]
460
return os.path.basename(self._tree_id_paths[trans_id])
462
raise NoFinalPath(trans_id, self)
465
"""Return a map of parent: children for known parents.
467
Only new paths and parents of tree files with assigned ids are used.
470
items = list(self._new_parent.iteritems())
471
items.extend((t, self.final_parent(t)) for t in
472
self._tree_id_paths.keys())
473
for trans_id, parent_id in items:
474
if parent_id not in by_parent:
475
by_parent[parent_id] = set()
476
by_parent[parent_id].add(trans_id)
479
def path_changed(self, trans_id):
480
"""Return True if a trans_id's path has changed."""
481
return (trans_id in self._new_name) or (trans_id in self._new_parent)
483
def new_contents(self, trans_id):
484
return (trans_id in self._new_contents)
486
def find_conflicts(self):
487
"""Find any violations of inventory or filesystem invariants"""
488
if self.__done is True:
489
raise ReusingTransform()
491
# ensure all children of all existent parents are known
492
# all children of non-existent parents are known, by definition.
493
self._add_tree_children()
494
by_parent = self.by_parent()
495
conflicts.extend(self._unversioned_parents(by_parent))
496
conflicts.extend(self._parent_loops())
497
conflicts.extend(self._duplicate_entries(by_parent))
498
conflicts.extend(self._duplicate_ids())
499
conflicts.extend(self._parent_type_conflicts(by_parent))
500
conflicts.extend(self._improper_versioning())
501
conflicts.extend(self._executability_conflicts())
502
conflicts.extend(self._overwrite_conflicts())
505
def _add_tree_children(self):
506
"""Add all the children of all active parents to the known paths.
508
Active parents are those which gain children, and those which are
509
removed. This is a necessary first step in detecting conflicts.
511
parents = self.by_parent().keys()
512
parents.extend([t for t in self._removed_contents if
513
self.tree_kind(t) == 'directory'])
514
for trans_id in self._removed_id:
515
file_id = self.tree_file_id(trans_id)
516
if self._tree.inventory[file_id].kind == 'directory':
517
parents.append(trans_id)
519
for parent_id in parents:
520
# ensure that all children are registered with the transaction
521
list(self.iter_tree_children(parent_id))
523
def iter_tree_children(self, parent_id):
524
"""Iterate through the entry's tree children, if any"""
526
path = self._tree_id_paths[parent_id]
530
children = os.listdir(self._tree.abspath(path))
532
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
536
for child in children:
537
childpath = joinpath(path, child)
538
if self._tree.is_control_filename(childpath):
540
yield self.trans_id_tree_path(childpath)
542
def has_named_child(self, by_parent, parent_id, name):
544
children = by_parent[parent_id]
547
for child in children:
548
if self.final_name(child) == name:
551
path = self._tree_id_paths[parent_id]
554
childpath = joinpath(path, name)
555
child_id = self._tree_path_ids.get(childpath)
557
return lexists(self._tree.abspath(childpath))
559
if self.final_parent(child_id) != parent_id:
561
if child_id in self._removed_contents:
562
# XXX What about dangling file-ids?
567
def _parent_loops(self):
568
"""No entry should be its own ancestor"""
570
for trans_id in self._new_parent:
573
while parent_id is not ROOT_PARENT:
576
parent_id = self.final_parent(parent_id)
579
if parent_id == trans_id:
580
conflicts.append(('parent loop', trans_id))
581
if parent_id in seen:
585
def _unversioned_parents(self, by_parent):
586
"""If parent directories are versioned, children must be versioned."""
588
for parent_id, children in by_parent.iteritems():
589
if parent_id is ROOT_PARENT:
591
if self.final_file_id(parent_id) is not None:
593
for child_id in children:
594
if self.final_file_id(child_id) is not None:
595
conflicts.append(('unversioned parent', parent_id))
599
def _improper_versioning(self):
600
"""Cannot version a file with no contents, or a bad type.
602
However, existing entries with no contents are okay.
605
for trans_id in self._new_id.iterkeys():
607
kind = self.final_kind(trans_id)
609
conflicts.append(('versioning no contents', trans_id))
611
if not InventoryEntry.versionable_kind(kind):
612
conflicts.append(('versioning bad kind', trans_id, kind))
615
def _executability_conflicts(self):
616
"""Check for bad executability changes.
618
Only versioned files may have their executability set, because
619
1. only versioned entries can have executability under windows
620
2. only files can be executable. (The execute bit on a directory
621
does not indicate searchability)
624
for trans_id in self._new_executability:
625
if self.final_file_id(trans_id) is None:
626
conflicts.append(('unversioned executability', trans_id))
629
non_file = self.final_kind(trans_id) != "file"
633
conflicts.append(('non-file executability', trans_id))
636
def _overwrite_conflicts(self):
637
"""Check for overwrites (not permitted on Win32)"""
639
for trans_id in self._new_contents:
641
self.tree_kind(trans_id)
644
if trans_id not in self._removed_contents:
645
conflicts.append(('overwrite', trans_id,
646
self.final_name(trans_id)))
649
def _duplicate_entries(self, by_parent):
650
"""No directory may have two entries with the same name."""
652
for children in by_parent.itervalues():
653
name_ids = [(self.final_name(t), t) for t in children]
657
for name, trans_id in name_ids:
659
kind = self.final_kind(trans_id)
662
file_id = self.final_file_id(trans_id)
663
if kind is None and file_id is None:
665
if name == last_name:
666
conflicts.append(('duplicate', last_trans_id, trans_id,
669
last_trans_id = trans_id
672
def _duplicate_ids(self):
673
"""Each inventory id may only be used once"""
675
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
677
active_tree_ids = set((f for f in self._tree.inventory if
678
f not in removed_tree_ids))
679
for trans_id, file_id in self._new_id.iteritems():
680
if file_id in active_tree_ids:
681
old_trans_id = self.trans_id_tree_file_id(file_id)
682
conflicts.append(('duplicate id', old_trans_id, trans_id))
685
def _parent_type_conflicts(self, by_parent):
686
"""parents must have directory 'contents'."""
688
for parent_id, children in by_parent.iteritems():
689
if parent_id is ROOT_PARENT:
691
if not self._any_contents(children):
693
for child in children:
695
self.final_kind(child)
699
kind = self.final_kind(parent_id)
703
conflicts.append(('missing parent', parent_id))
704
elif kind != "directory":
705
conflicts.append(('non-directory parent', parent_id))
708
def _any_contents(self, trans_ids):
709
"""Return true if any of the trans_ids, will have contents."""
710
for trans_id in trans_ids:
712
kind = self.final_kind(trans_id)
719
"""Apply all changes to the inventory and filesystem.
721
If filesystem or inventory conflicts are present, MalformedTransform
724
conflicts = self.find_conflicts()
725
if len(conflicts) != 0:
726
raise MalformedTransform(conflicts=conflicts)
728
inv = self._tree.inventory
729
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
731
child_pb.update('Apply phase', 0, 2)
732
self._apply_removals(inv, limbo_inv)
733
child_pb.update('Apply phase', 1, 2)
734
modified_paths = self._apply_insertions(inv, limbo_inv)
737
self._tree._write_inventory(inv)
740
return _TransformResults(modified_paths)
742
def _limbo_name(self, trans_id):
743
"""Generate the limbo name of a file"""
744
return pathjoin(self._limbodir, trans_id)
746
def _apply_removals(self, inv, limbo_inv):
747
"""Perform tree operations that remove directory/inventory names.
749
That is, delete files that are to be deleted, and put any files that
750
need renaming into limbo. This must be done in strict child-to-parent
753
tree_paths = list(self._tree_path_ids.iteritems())
754
tree_paths.sort(reverse=True)
755
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
757
for num, data in enumerate(tree_paths):
758
path, trans_id = data
759
child_pb.update('removing file', num, len(tree_paths))
760
full_path = self._tree.abspath(path)
761
if trans_id in self._removed_contents:
762
delete_any(full_path)
763
elif trans_id in self._new_name or trans_id in \
766
os.rename(full_path, self._limbo_name(trans_id))
768
if e.errno != errno.ENOENT:
770
if trans_id in self._removed_id:
771
if trans_id == self._new_root:
772
file_id = self._tree.inventory.root.file_id
774
file_id = self.tree_file_id(trans_id)
776
elif trans_id in self._new_name or trans_id in self._new_parent:
777
file_id = self.tree_file_id(trans_id)
778
if file_id is not None:
779
limbo_inv[trans_id] = inv[file_id]
784
def _apply_insertions(self, inv, limbo_inv):
785
"""Perform tree operations that insert directory/inventory names.
787
That is, create any files that need to be created, and restore from
788
limbo any files that needed renaming. This must be done in strict
789
parent-to-child order.
791
new_paths = self.new_paths()
793
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
795
for num, (path, trans_id) in enumerate(new_paths):
796
child_pb.update('adding file', num, len(new_paths))
798
kind = self._new_contents[trans_id]
800
kind = contents = None
801
if trans_id in self._new_contents or \
802
self.path_changed(trans_id):
803
full_path = self._tree.abspath(path)
805
os.rename(self._limbo_name(trans_id), full_path)
807
# We may be renaming a dangling inventory id
808
if e.errno != errno.ENOENT:
810
if trans_id in self._new_contents:
811
modified_paths.append(full_path)
812
del self._new_contents[trans_id]
814
if trans_id in self._new_id:
816
kind = file_kind(self._tree.abspath(path))
817
inv.add_path(path, kind, self._new_id[trans_id])
818
elif trans_id in self._new_name or trans_id in\
820
entry = limbo_inv.get(trans_id)
821
if entry is not None:
822
entry.name = self.final_name(trans_id)
823
parent_path = os.path.dirname(path)
825
self._tree.inventory.path2id(parent_path)
828
# requires files and inventory entries to be in place
829
if trans_id in self._new_executability:
830
self._set_executability(path, inv, trans_id)
833
return modified_paths
835
def _set_executability(self, path, inv, trans_id):
836
"""Set the executability of versioned files """
837
file_id = inv.path2id(path)
838
new_executability = self._new_executability[trans_id]
839
inv[file_id].executable = new_executability
840
if supports_executable():
841
abspath = self._tree.abspath(path)
842
current_mode = os.stat(abspath).st_mode
843
if new_executability:
846
to_mode = current_mode | (0100 & ~umask)
847
# Enable x-bit for others only if they can read it.
848
if current_mode & 0004:
849
to_mode |= 0001 & ~umask
850
if current_mode & 0040:
851
to_mode |= 0010 & ~umask
853
to_mode = current_mode & ~0111
854
os.chmod(abspath, to_mode)
856
def _new_entry(self, name, parent_id, file_id):
857
"""Helper function to create a new filesystem entry."""
858
trans_id = self.create_path(name, parent_id)
859
if file_id is not None:
860
self.version_file(file_id, trans_id)
863
def new_file(self, name, parent_id, contents, file_id=None,
865
"""Convenience method to create files.
867
name is the name of the file to create.
868
parent_id is the transaction id of the parent directory of the file.
869
contents is an iterator of bytestrings, which will be used to produce
871
:param file_id: The inventory ID of the file, if it is to be versioned.
872
:param executable: Only valid when a file_id has been supplied.
874
trans_id = self._new_entry(name, parent_id, file_id)
875
# TODO: rather than scheduling a set_executable call,
876
# have create_file create the file with the right mode.
877
self.create_file(contents, trans_id)
878
if executable is not None:
879
self.set_executability(executable, trans_id)
882
def new_directory(self, name, parent_id, file_id=None):
883
"""Convenience method to create directories.
885
name is the name of the directory to create.
886
parent_id is the transaction id of the parent directory of the
888
file_id is the inventory ID of the directory, if it is to be versioned.
890
trans_id = self._new_entry(name, parent_id, file_id)
891
self.create_directory(trans_id)
894
def new_symlink(self, name, parent_id, target, file_id=None):
895
"""Convenience method to create symbolic link.
897
name is the name of the symlink to create.
898
parent_id is the transaction id of the parent directory of the symlink.
899
target is a bytestring of the target of the symlink.
900
file_id is the inventory ID of the file, if it is to be versioned.
902
trans_id = self._new_entry(name, parent_id, file_id)
903
self.create_symlink(target, trans_id)
906
def _iter_changes(self):
907
"""Produce output in the same format as Tree._iter_changes.
909
Will produce nonsensical results if invoked while inventory/filesystem
910
conflicts (as reported by TreeTransform.find_conflicts()) are present.
912
This reads the Transform, but only reproduces changes involving a
913
file_id. Files that are not versioned in either of the FROM or TO
914
states are not reflected.
916
final_paths = FinalPaths(self)
917
trans_ids = set(self._removed_id)
918
trans_ids.update(self._new_id.keys())
919
trans_ids.update(self._removed_contents)
920
trans_ids.update(self._new_contents.keys())
921
trans_ids.update(self._new_executability.keys())
922
trans_ids.update(self._new_name.keys())
923
trans_ids.update(self._new_parent.keys())
926
# Build up two dicts: trans_ids associated with file ids in the
927
# FROM state, vs the TO state.
928
for trans_id in trans_ids:
929
from_file_id = self.tree_file_id(trans_id)
930
if from_file_id is not None:
931
from_trans_ids[from_file_id] = trans_id
932
to_file_id = self.final_file_id(trans_id)
933
if to_file_id is not None:
934
to_trans_ids[to_file_id] = trans_id
938
# Now iterate through all active file_ids
939
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
941
from_trans_id = from_trans_ids.get(file_id)
942
# find file ids, and determine versioning state
943
if from_trans_id is None:
944
from_versioned = False
945
from_trans_id = to_trans_ids[file_id]
947
from_versioned = True
948
to_trans_id = to_trans_ids.get(file_id)
949
if to_trans_id is None:
951
to_trans_id = from_trans_id
954
from_path = self._tree_id_paths.get(from_trans_id)
956
# get data from working tree if versioned
957
from_entry = self._tree.inventory[file_id]
958
from_name = from_entry.name
959
from_parent = from_entry.parent_id
962
if from_path is None:
963
# File does not exist in FROM state
967
# File exists, but is not versioned. Have to use path-
969
from_name = os.path.basename(from_path)
970
tree_parent = self.get_tree_parent(from_trans_id)
971
from_parent = self.tree_file_id(tree_parent)
972
if from_path is not None:
973
from_kind, from_executable, from_stats = \
974
self._tree._comparison_data(from_entry, from_path)
977
from_executable = False
978
to_name = self.final_name(to_trans_id)
980
to_kind = self.final_kind(to_trans_id)
983
to_parent = self.final_file_id(self.final_parent(to_trans_id))
984
if to_trans_id in self._new_executability:
985
to_executable = self._new_executability[to_trans_id]
986
elif to_trans_id == from_trans_id:
987
to_executable = from_executable
989
to_executable = False
990
to_path = final_paths.get_path(to_trans_id)
991
if from_kind != to_kind:
993
elif to_kind in ('file' or 'symlink') and (
994
to_trans_id != from_trans_id or
995
to_trans_id in self._new_contents):
997
if (not modified and from_versioned == to_versioned and
998
from_parent==to_parent and from_name == to_name and
999
from_executable == to_executable):
1001
results.append((file_id, to_path, modified,
1002
(from_versioned, to_versioned),
1003
(from_parent, to_parent),
1004
(from_name, to_name),
1005
(from_kind, to_kind),
1006
(from_executable, to_executable)))
1007
return iter(sorted(results, key=lambda x:x[1]))
1010
def joinpath(parent, child):
1011
"""Join tree-relative paths, handling the tree root specially"""
1012
if parent is None or parent == "":
1015
return pathjoin(parent, child)
1018
class FinalPaths(object):
1019
"""Make path calculation cheap by memoizing paths.
1021
The underlying tree must not be manipulated between calls, or else
1022
the results will likely be incorrect.
1024
def __init__(self, transform):
1025
object.__init__(self)
1026
self._known_paths = {}
1027
self.transform = transform
1029
def _determine_path(self, trans_id):
1030
if trans_id == self.transform.root:
1032
name = self.transform.final_name(trans_id)
1033
parent_id = self.transform.final_parent(trans_id)
1034
if parent_id == self.transform.root:
1037
return pathjoin(self.get_path(parent_id), name)
1039
def get_path(self, trans_id):
1040
"""Find the final path associated with a trans_id"""
1041
if trans_id not in self._known_paths:
1042
self._known_paths[trans_id] = self._determine_path(trans_id)
1043
return self._known_paths[trans_id]
1045
def topology_sorted_ids(tree):
1046
"""Determine the topological order of the ids in a tree"""
1047
file_ids = list(tree)
1048
file_ids.sort(key=tree.id2path)
1052
def build_tree(tree, wt):
1053
"""Create working tree for a branch, using a TreeTransform.
1055
This function should be used on empty trees, having a tree root at most.
1056
(see merge and revert functionality for working with existing trees)
1058
Existing files are handled like so:
1060
- Existing bzrdirs take precedence over creating new items. They are
1061
created as '%s.diverted' % name.
1062
- Otherwise, if the content on disk matches the content we are building,
1063
it is silently replaced.
1064
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1066
if len(wt.inventory) > 1: # more than just a root
1067
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1069
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1070
pp = ProgressPhase("Build phase", 2, top_pb)
1071
if tree.inventory.root is not None:
1072
wt.set_root_id(tree.inventory.root.file_id)
1073
tt = TreeTransform(wt)
1077
file_trans_id[wt.get_root_id()] = \
1078
tt.trans_id_tree_file_id(wt.get_root_id())
1079
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1081
for num, (tree_path, entry) in \
1082
enumerate(tree.inventory.iter_entries_by_dir()):
1083
pb.update("Building tree", num, len(tree.inventory))
1084
if entry.parent_id is None:
1087
file_id = entry.file_id
1088
target_path = wt.abspath(tree_path)
1090
kind = file_kind(target_path)
1094
if kind == "directory":
1096
bzrdir.BzrDir.open(target_path)
1097
except errors.NotBranchError:
1101
if (file_id not in divert and
1102
_content_match(tree, entry, file_id, kind,
1104
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1105
if kind == 'directory':
1107
if entry.parent_id not in file_trans_id:
1108
raise repr(entry.parent_id)
1109
parent_id = file_trans_id[entry.parent_id]
1110
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1113
new_trans_id = file_trans_id[file_id]
1114
old_parent = tt.trans_id_tree_path(tree_path)
1115
_reparent_children(tt, old_parent, new_trans_id)
1119
divert_trans = set(file_trans_id[f] for f in divert)
1120
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1121
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1122
conflicts = cook_conflicts(raw_conflicts, tt)
1123
for conflict in conflicts:
1126
wt.add_conflicts(conflicts)
1127
except errors.UnsupportedOperation:
1135
def _reparent_children(tt, old_parent, new_parent):
1136
for child in tt.iter_tree_children(old_parent):
1137
tt.adjust_path(tt.final_name(child), new_parent, child)
1140
def _content_match(tree, entry, file_id, kind, target_path):
1141
if entry.kind != kind:
1143
if entry.kind == "directory":
1145
if entry.kind == "file":
1146
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1148
elif entry.kind == "symlink":
1149
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1154
def resolve_checkout(tt, conflicts, divert):
1155
new_conflicts = set()
1156
for c_type, conflict in ((c[0], c) for c in conflicts):
1157
# Anything but a 'duplicate' would indicate programmer error
1158
assert c_type == 'duplicate', c_type
1159
# Now figure out which is new and which is old
1160
if tt.new_contents(conflict[1]):
1161
new_file = conflict[1]
1162
old_file = conflict[2]
1164
new_file = conflict[2]
1165
old_file = conflict[1]
1167
# We should only get here if the conflict wasn't completely
1169
final_parent = tt.final_parent(old_file)
1170
if new_file in divert:
1171
new_name = tt.final_name(old_file)+'.diverted'
1172
tt.adjust_path(new_name, final_parent, new_file)
1173
new_conflicts.add((c_type, 'Diverted to',
1174
new_file, old_file))
1176
new_name = tt.final_name(old_file)+'.moved'
1177
tt.adjust_path(new_name, final_parent, old_file)
1178
new_conflicts.add((c_type, 'Moved existing file to',
1179
old_file, new_file))
1180
return new_conflicts
1183
def new_by_entry(tt, entry, parent_id, tree):
1184
"""Create a new file according to its inventory entry"""
1188
contents = tree.get_file(entry.file_id).readlines()
1189
executable = tree.is_executable(entry.file_id)
1190
return tt.new_file(name, parent_id, contents, entry.file_id,
1192
elif kind == 'directory':
1193
return tt.new_directory(name, parent_id, entry.file_id)
1194
elif kind == 'symlink':
1195
target = tree.get_symlink_target(entry.file_id)
1196
return tt.new_symlink(name, parent_id, target, entry.file_id)
1198
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1199
"""Create new file contents according to an inventory entry."""
1200
if entry.kind == "file":
1202
lines = tree.get_file(entry.file_id).readlines()
1203
tt.create_file(lines, trans_id, mode_id=mode_id)
1204
elif entry.kind == "symlink":
1205
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1206
elif entry.kind == "directory":
1207
tt.create_directory(trans_id)
1209
def create_entry_executability(tt, entry, trans_id):
1210
"""Set the executability of a trans_id according to an inventory entry"""
1211
if entry.kind == "file":
1212
tt.set_executability(entry.executable, trans_id)
1215
def find_interesting(working_tree, target_tree, filenames):
1216
"""Find the ids corresponding to specified filenames."""
1217
trees = (working_tree, target_tree)
1218
return tree.find_ids_across_trees(filenames, trees)
1221
def change_entry(tt, file_id, working_tree, target_tree,
1222
trans_id_file_id, backups, trans_id, by_parent):
1223
"""Replace a file_id's contents with those from a target tree."""
1224
e_trans_id = trans_id_file_id(file_id)
1225
entry = target_tree.inventory[file_id]
1226
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1229
mode_id = e_trans_id
1232
tt.delete_contents(e_trans_id)
1234
parent_trans_id = trans_id_file_id(entry.parent_id)
1235
backup_name = get_backup_name(entry, by_parent,
1236
parent_trans_id, tt)
1237
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1238
tt.unversion_file(e_trans_id)
1239
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1240
tt.version_file(file_id, e_trans_id)
1241
trans_id[file_id] = e_trans_id
1242
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1243
create_entry_executability(tt, entry, e_trans_id)
1246
tt.set_executability(entry.executable, e_trans_id)
1247
if tt.final_name(e_trans_id) != entry.name:
1250
parent_id = tt.final_parent(e_trans_id)
1251
parent_file_id = tt.final_file_id(parent_id)
1252
if parent_file_id != entry.parent_id:
1257
parent_trans_id = trans_id_file_id(entry.parent_id)
1258
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1261
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1262
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1265
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1266
"""Produce a backup-style name that appears to be available"""
1270
yield "%s.~%d~" % (name, counter)
1272
for new_name in name_gen():
1273
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1277
def _entry_changes(file_id, entry, working_tree):
1278
"""Determine in which ways the inventory entry has changed.
1280
Returns booleans: has_contents, content_mod, meta_mod
1281
has_contents means there are currently contents, but they differ
1282
contents_mod means contents need to be modified
1283
meta_mod means the metadata needs to be modified
1285
cur_entry = working_tree.inventory[file_id]
1287
working_kind = working_tree.kind(file_id)
1290
has_contents = False
1293
if has_contents is True:
1294
if entry.kind != working_kind:
1295
contents_mod, meta_mod = True, False
1297
cur_entry._read_tree_state(working_tree.id2path(file_id),
1299
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1300
cur_entry._forget_tree_state()
1301
return has_contents, contents_mod, meta_mod
1304
def revert(working_tree, target_tree, filenames, backups=False,
1305
pb=DummyProgress(), change_reporter=None):
1306
"""Revert a working tree's contents to those of a target tree."""
1307
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1308
tt = TreeTransform(working_tree, pb)
1310
pp = ProgressPhase("Revert phase", 3, pb)
1312
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1314
_alter_files(working_tree, target_tree, tt, child_pb,
1315
interesting_ids, backups)
1319
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1321
raw_conflicts = resolve_conflicts(tt, child_pb)
1324
conflicts = cook_conflicts(raw_conflicts, tt)
1326
from bzrlib import delta
1327
change_reporter = delta.ChangeReporter(working_tree.inventory)
1328
delta.report_changes(tt._iter_changes(), change_reporter)
1329
for conflict in conflicts:
1333
working_tree.set_merge_modified({})
1340
def _alter_files(working_tree, target_tree, tt, pb, interesting_ids,
1342
from bzrlib import delta
1343
merge_modified = working_tree.merge_modified()
1344
change_list = target_tree._iter_changes(working_tree,
1345
specific_file_ids=interesting_ids, pb=pb)
1346
if target_tree.inventory.root is None:
1351
for id_num, (file_id, path, changed_content, versioned, parent, name,
1352
kind, executable) in enumerate(change_list):
1353
if skip_root and file_id[0] is not None and parent[0] is None:
1355
trans_id = tt.trans_id_file_id(file_id)
1358
keep_content = False
1359
if kind[0] == 'file' and (backups or kind[1] is None):
1360
wt_sha1 = working_tree.get_file_sha1(file_id)
1361
if merge_modified.get(file_id) != wt_sha1:
1362
if basis_tree is None:
1363
basis_tree = working_tree.basis_tree()
1364
if file_id in basis_tree:
1365
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1367
elif kind[1] is None and not versioned[1]:
1369
if kind[0] is not None:
1370
if not keep_content:
1371
tt.delete_contents(trans_id)
1372
elif kind[1] is not None:
1373
parent_trans_id = tt.trans_id_file_id(parent[0])
1374
by_parent = tt.by_parent()
1375
backup_name = _get_backup_name(name[0], by_parent,
1376
parent_trans_id, tt)
1377
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1378
new_trans_id = tt.create_path(name[0], parent_trans_id)
1379
if versioned == (True, True):
1380
tt.unversion_file(trans_id)
1381
tt.version_file(file_id, new_trans_id)
1382
# New contents should have the same unix perms as old
1385
trans_id = new_trans_id
1386
if kind[1] == 'directory':
1387
tt.create_directory(trans_id)
1388
elif kind[1] == 'symlink':
1389
tt.create_symlink(target_tree.get_symlink_target(file_id),
1391
elif kind[1] == 'file':
1392
tt.create_file(target_tree.get_file_lines(file_id),
1394
# preserve the execute bit when backing up
1395
if keep_content and executable[0] == executable[1]:
1396
tt.set_executability(executable[1], trans_id)
1398
assert kind[1] is None
1399
if versioned == (False, True):
1400
tt.version_file(file_id, trans_id)
1401
if versioned == (True, False):
1402
tt.unversion_file(trans_id)
1403
if (name[1] is not None and
1404
(name[0] != name[1] or parent[0] != parent[1])):
1405
tt.adjust_path(name[1], tt.trans_id_file_id(parent[1]), trans_id)
1406
if executable[0] != executable[1] and kind[1] == "file":
1407
tt.set_executability(executable[1], trans_id)
1410
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1411
"""Make many conflict-resolution attempts, but die if they fail"""
1412
if pass_func is None:
1413
pass_func = conflict_pass
1414
new_conflicts = set()
1417
pb.update('Resolution pass', n+1, 10)
1418
conflicts = tt.find_conflicts()
1419
if len(conflicts) == 0:
1420
return new_conflicts
1421
new_conflicts.update(pass_func(tt, conflicts))
1422
raise MalformedTransform(conflicts=conflicts)
1427
def conflict_pass(tt, conflicts):
1428
"""Resolve some classes of conflicts."""
1429
new_conflicts = set()
1430
for c_type, conflict in ((c[0], c) for c in conflicts):
1431
if c_type == 'duplicate id':
1432
tt.unversion_file(conflict[1])
1433
new_conflicts.add((c_type, 'Unversioned existing file',
1434
conflict[1], conflict[2], ))
1435
elif c_type == 'duplicate':
1436
# files that were renamed take precedence
1437
new_name = tt.final_name(conflict[1])+'.moved'
1438
final_parent = tt.final_parent(conflict[1])
1439
if tt.path_changed(conflict[1]):
1440
tt.adjust_path(new_name, final_parent, conflict[2])
1441
new_conflicts.add((c_type, 'Moved existing file to',
1442
conflict[2], conflict[1]))
1444
tt.adjust_path(new_name, final_parent, conflict[1])
1445
new_conflicts.add((c_type, 'Moved existing file to',
1446
conflict[1], conflict[2]))
1447
elif c_type == 'parent loop':
1448
# break the loop by undoing one of the ops that caused the loop
1450
while not tt.path_changed(cur):
1451
cur = tt.final_parent(cur)
1452
new_conflicts.add((c_type, 'Cancelled move', cur,
1453
tt.final_parent(cur),))
1454
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1456
elif c_type == 'missing parent':
1457
trans_id = conflict[1]
1459
tt.cancel_deletion(trans_id)
1460
new_conflicts.add(('deleting parent', 'Not deleting',
1463
tt.create_directory(trans_id)
1464
new_conflicts.add((c_type, 'Created directory', trans_id))
1465
elif c_type == 'unversioned parent':
1466
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1467
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1468
return new_conflicts
1471
def cook_conflicts(raw_conflicts, tt):
1472
"""Generate a list of cooked conflicts, sorted by file path"""
1473
from bzrlib.conflicts import Conflict
1474
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1475
return sorted(conflict_iter, key=Conflict.sort_key)
1478
def iter_cook_conflicts(raw_conflicts, tt):
1479
from bzrlib.conflicts import Conflict
1481
for conflict in raw_conflicts:
1482
c_type = conflict[0]
1483
action = conflict[1]
1484
modified_path = fp.get_path(conflict[2])
1485
modified_id = tt.final_file_id(conflict[2])
1486
if len(conflict) == 3:
1487
yield Conflict.factory(c_type, action=action, path=modified_path,
1488
file_id=modified_id)
1491
conflicting_path = fp.get_path(conflict[3])
1492
conflicting_id = tt.final_file_id(conflict[3])
1493
yield Conflict.factory(c_type, action=action, path=modified_path,
1494
file_id=modified_id,
1495
conflict_path=conflicting_path,
1496
conflict_file_id=conflicting_id)