1
# Copyright (C) 2006 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from stat import S_ISREG
21
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
22
ReusingTransform, NotVersionedError, CantMoveRoot,
23
ExistingLimbo, ImmortalLimbo)
24
from bzrlib.inventory import InventoryEntry
25
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
27
from bzrlib.progress import DummyProgress, ProgressPhase
28
from bzrlib.trace import mutter, warning
30
import bzrlib.urlutils as urlutils
33
ROOT_PARENT = "root-parent"
36
def unique_add(map, key, value):
38
raise DuplicateKey(key=key)
42
class _TransformResults(object):
43
def __init__(self, modified_paths):
45
self.modified_paths = modified_paths
48
class TreeTransform(object):
49
"""Represent a tree transformation.
51
This object is designed to support incremental generation of the transform,
54
It is easy to produce malformed transforms, but they are generally
55
harmless. Attempting to apply a malformed transform will cause an
56
exception to be raised before any modifications are made to the tree.
58
Many kinds of malformed transforms can be corrected with the
59
resolve_conflicts function. The remaining ones indicate programming error,
60
such as trying to create a file with no path.
62
Two sets of file creation methods are supplied. Convenience methods are:
67
These are composed of the low-level methods:
69
* create_file or create_directory or create_symlink
73
def __init__(self, tree, pb=DummyProgress()):
74
"""Note: a write lock is taken on the tree.
76
Use TreeTransform.finalize() to release the lock
80
self._tree.lock_write()
82
control_files = self._tree._control_files
83
self._limbodir = urlutils.local_path_from_url(
84
control_files.controlfilename('limbo'))
86
os.mkdir(self._limbodir)
88
if e.errno == errno.EEXIST:
89
raise ExistingLimbo(self._limbodir)
97
self._new_contents = {}
98
self._removed_contents = set()
99
self._new_executability = {}
101
self._non_present_ids = {}
103
self._removed_id = set()
104
self._tree_path_ids = {}
105
self._tree_id_paths = {}
106
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
110
def __get_root(self):
111
return self._new_root
113
root = property(__get_root)
116
"""Release the working tree lock, if held, clean up limbo dir."""
117
if self._tree is None:
120
for trans_id, kind in self._new_contents.iteritems():
121
path = self._limbo_name(trans_id)
122
if kind == "directory":
127
os.rmdir(self._limbodir)
129
# We don't especially care *why* the dir is immortal.
130
raise ImmortalLimbo(self._limbodir)
135
def _assign_id(self):
136
"""Produce a new tranform id"""
137
new_id = "new-%s" % self._id_number
141
def create_path(self, name, parent):
142
"""Assign a transaction id to a new path"""
143
trans_id = self._assign_id()
144
unique_add(self._new_name, trans_id, name)
145
unique_add(self._new_parent, trans_id, parent)
148
def adjust_path(self, name, parent, trans_id):
149
"""Change the path that is assigned to a transaction id."""
150
if trans_id == self._new_root:
152
self._new_name[trans_id] = name
153
self._new_parent[trans_id] = parent
155
def adjust_root_path(self, name, parent):
156
"""Emulate moving the root by moving all children, instead.
158
We do this by undoing the association of root's transaction id with the
159
current tree. This allows us to create a new directory with that
160
transaction id. We unversion the root directory and version the
161
physically new directory, and hope someone versions the tree root
164
old_root = self._new_root
165
old_root_file_id = self.final_file_id(old_root)
166
# force moving all children of root
167
for child_id in self.iter_tree_children(old_root):
168
if child_id != parent:
169
self.adjust_path(self.final_name(child_id),
170
self.final_parent(child_id), child_id)
171
file_id = self.final_file_id(child_id)
172
if file_id is not None:
173
self.unversion_file(child_id)
174
self.version_file(file_id, child_id)
176
# the physical root needs a new transaction id
177
self._tree_path_ids.pop("")
178
self._tree_id_paths.pop(old_root)
179
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
180
if parent == old_root:
181
parent = self._new_root
182
self.adjust_path(name, parent, old_root)
183
self.create_directory(old_root)
184
self.version_file(old_root_file_id, old_root)
185
self.unversion_file(self._new_root)
187
def trans_id_tree_file_id(self, inventory_id):
188
"""Determine the transaction id of a working tree file.
190
This reflects only files that already exist, not ones that will be
191
added by transactions.
193
path = self._tree.inventory.id2path(inventory_id)
194
return self.trans_id_tree_path(path)
196
def trans_id_file_id(self, file_id):
197
"""Determine or set the transaction id associated with a file ID.
198
A new id is only created for file_ids that were never present. If
199
a transaction has been unversioned, it is deliberately still returned.
200
(this will likely lead to an unversioned parent conflict.)
202
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
203
return self._r_new_id[file_id]
204
elif file_id in self._tree.inventory:
205
return self.trans_id_tree_file_id(file_id)
206
elif file_id in self._non_present_ids:
207
return self._non_present_ids[file_id]
209
trans_id = self._assign_id()
210
self._non_present_ids[file_id] = trans_id
213
def canonical_path(self, path):
214
"""Get the canonical tree-relative path"""
215
# don't follow final symlinks
216
dirname, basename = os.path.split(self._tree.abspath(path))
217
dirname = os.path.realpath(dirname)
218
return self._tree.relpath(pathjoin(dirname, basename))
220
def trans_id_tree_path(self, path):
221
"""Determine (and maybe set) the transaction ID for a tree path."""
222
path = self.canonical_path(path)
223
if path not in self._tree_path_ids:
224
self._tree_path_ids[path] = self._assign_id()
225
self._tree_id_paths[self._tree_path_ids[path]] = path
226
return self._tree_path_ids[path]
228
def get_tree_parent(self, trans_id):
229
"""Determine id of the parent in the tree."""
230
path = self._tree_id_paths[trans_id]
233
return self.trans_id_tree_path(os.path.dirname(path))
235
def create_file(self, contents, trans_id, mode_id=None):
236
"""Schedule creation of a new file.
240
Contents is an iterator of strings, all of which will be written
241
to the target destination.
243
New file takes the permissions of any existing file with that id,
244
unless mode_id is specified.
246
f = file(self._limbo_name(trans_id), 'wb')
247
unique_add(self._new_contents, trans_id, 'file')
248
for segment in contents:
251
self._set_mode(trans_id, mode_id, S_ISREG)
253
def _set_mode(self, trans_id, mode_id, typefunc):
254
"""Set the mode of new file contents.
255
The mode_id is the existing file to get the mode from (often the same
256
as trans_id). The operation is only performed if there's a mode match
257
according to typefunc.
262
old_path = self._tree_id_paths[mode_id]
266
mode = os.stat(old_path).st_mode
268
if e.errno == errno.ENOENT:
273
os.chmod(self._limbo_name(trans_id), mode)
275
def create_directory(self, trans_id):
276
"""Schedule creation of a new directory.
278
See also new_directory.
280
os.mkdir(self._limbo_name(trans_id))
281
unique_add(self._new_contents, trans_id, 'directory')
283
def create_symlink(self, target, trans_id):
284
"""Schedule creation of a new symbolic link.
286
target is a bytestring.
287
See also new_symlink.
289
os.symlink(target, self._limbo_name(trans_id))
290
unique_add(self._new_contents, trans_id, 'symlink')
292
def cancel_creation(self, trans_id):
293
"""Cancel the creation of new file contents."""
294
del self._new_contents[trans_id]
295
delete_any(self._limbo_name(trans_id))
297
def delete_contents(self, trans_id):
298
"""Schedule the contents of a path entry for deletion"""
299
self.tree_kind(trans_id)
300
self._removed_contents.add(trans_id)
302
def cancel_deletion(self, trans_id):
303
"""Cancel a scheduled deletion"""
304
self._removed_contents.remove(trans_id)
306
def unversion_file(self, trans_id):
307
"""Schedule a path entry to become unversioned"""
308
self._removed_id.add(trans_id)
310
def delete_versioned(self, trans_id):
311
"""Delete and unversion a versioned file"""
312
self.delete_contents(trans_id)
313
self.unversion_file(trans_id)
315
def set_executability(self, executability, trans_id):
316
"""Schedule setting of the 'execute' bit
317
To unschedule, set to None
319
if executability is None:
320
del self._new_executability[trans_id]
322
unique_add(self._new_executability, trans_id, executability)
324
def version_file(self, file_id, trans_id):
325
"""Schedule a file to become versioned."""
326
assert file_id is not None
327
unique_add(self._new_id, trans_id, file_id)
328
unique_add(self._r_new_id, file_id, trans_id)
330
def cancel_versioning(self, trans_id):
331
"""Undo a previous versioning of a file"""
332
file_id = self._new_id[trans_id]
333
del self._new_id[trans_id]
334
del self._r_new_id[file_id]
337
"""Determine the paths of all new and changed files"""
339
fp = FinalPaths(self)
340
for id_set in (self._new_name, self._new_parent, self._new_contents,
341
self._new_id, self._new_executability):
342
new_ids.update(id_set)
343
new_paths = [(fp.get_path(t), t) for t in new_ids]
347
def tree_kind(self, trans_id):
348
"""Determine the file kind in the working tree.
350
Raises NoSuchFile if the file does not exist
352
path = self._tree_id_paths.get(trans_id)
354
raise NoSuchFile(None)
356
return file_kind(self._tree.abspath(path))
358
if e.errno != errno.ENOENT:
361
raise NoSuchFile(path)
363
def final_kind(self, trans_id):
364
"""Determine the final file kind, after any changes applied.
366
Raises NoSuchFile if the file does not exist/has no contents.
367
(It is conceivable that a path would be created without the
368
corresponding contents insertion command)
370
if trans_id in self._new_contents:
371
return self._new_contents[trans_id]
372
elif trans_id in self._removed_contents:
373
raise NoSuchFile(None)
375
return self.tree_kind(trans_id)
377
def tree_file_id(self, trans_id):
378
"""Determine the file id associated with the trans_id in the tree"""
380
path = self._tree_id_paths[trans_id]
382
# the file is a new, unversioned file, or invalid trans_id
384
# the file is old; the old id is still valid
385
if self._new_root == trans_id:
386
return self._tree.inventory.root.file_id
387
return self._tree.inventory.path2id(path)
389
def final_file_id(self, trans_id):
390
"""Determine the file id after any changes are applied, or None.
392
None indicates that the file will not be versioned after changes are
396
# there is a new id for this file
397
assert self._new_id[trans_id] is not None
398
return self._new_id[trans_id]
400
if trans_id in self._removed_id:
402
return self.tree_file_id(trans_id)
404
def inactive_file_id(self, trans_id):
405
"""Return the inactive file_id associated with a transaction id.
406
That is, the one in the tree or in non_present_ids.
407
The file_id may actually be active, too.
409
file_id = self.tree_file_id(trans_id)
410
if file_id is not None:
412
for key, value in self._non_present_ids.iteritems():
413
if value == trans_id:
416
def final_parent(self, trans_id):
417
"""Determine the parent file_id, after any changes are applied.
419
ROOT_PARENT is returned for the tree root.
422
return self._new_parent[trans_id]
424
return self.get_tree_parent(trans_id)
426
def final_name(self, trans_id):
427
"""Determine the final filename, after all changes are applied."""
429
return self._new_name[trans_id]
431
return os.path.basename(self._tree_id_paths[trans_id])
434
"""Return a map of parent: children for known parents.
436
Only new paths and parents of tree files with assigned ids are used.
439
items = list(self._new_parent.iteritems())
440
items.extend((t, self.final_parent(t)) for t in
441
self._tree_id_paths.keys())
442
for trans_id, parent_id in items:
443
if parent_id not in by_parent:
444
by_parent[parent_id] = set()
445
by_parent[parent_id].add(trans_id)
448
def path_changed(self, trans_id):
449
"""Return True if a trans_id's path has changed."""
450
return trans_id in self._new_name or trans_id in self._new_parent
452
def find_conflicts(self):
453
"""Find any violations of inventory or filesystem invariants"""
454
if self.__done is True:
455
raise ReusingTransform()
457
# ensure all children of all existent parents are known
458
# all children of non-existent parents are known, by definition.
459
self._add_tree_children()
460
by_parent = self.by_parent()
461
conflicts.extend(self._unversioned_parents(by_parent))
462
conflicts.extend(self._parent_loops())
463
conflicts.extend(self._duplicate_entries(by_parent))
464
conflicts.extend(self._duplicate_ids())
465
conflicts.extend(self._parent_type_conflicts(by_parent))
466
conflicts.extend(self._improper_versioning())
467
conflicts.extend(self._executability_conflicts())
468
conflicts.extend(self._overwrite_conflicts())
471
def _add_tree_children(self):
472
"""Add all the children of all active parents to the known paths.
474
Active parents are those which gain children, and those which are
475
removed. This is a necessary first step in detecting conflicts.
477
parents = self.by_parent().keys()
478
parents.extend([t for t in self._removed_contents if
479
self.tree_kind(t) == 'directory'])
480
for trans_id in self._removed_id:
481
file_id = self.tree_file_id(trans_id)
482
if self._tree.inventory[file_id].kind in ('directory',
484
parents.append(trans_id)
486
for parent_id in parents:
487
# ensure that all children are registered with the transaction
488
list(self.iter_tree_children(parent_id))
490
def iter_tree_children(self, parent_id):
491
"""Iterate through the entry's tree children, if any"""
493
path = self._tree_id_paths[parent_id]
497
children = os.listdir(self._tree.abspath(path))
499
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
503
for child in children:
504
childpath = joinpath(path, child)
505
if self._tree.is_control_filename(childpath):
507
yield self.trans_id_tree_path(childpath)
509
def has_named_child(self, by_parent, parent_id, name):
511
children = by_parent[parent_id]
514
for child in children:
515
if self.final_name(child) == name:
518
path = self._tree_id_paths[parent_id]
521
childpath = joinpath(path, name)
522
child_id = self._tree_path_ids.get(childpath)
524
return lexists(self._tree.abspath(childpath))
526
if tt.final_parent(child_id) != parent_id:
528
if child_id in tt._removed_contents:
529
# XXX What about dangling file-ids?
534
def _parent_loops(self):
535
"""No entry should be its own ancestor"""
537
for trans_id in self._new_parent:
540
while parent_id is not ROOT_PARENT:
542
parent_id = self.final_parent(parent_id)
543
if parent_id == trans_id:
544
conflicts.append(('parent loop', trans_id))
545
if parent_id in seen:
549
def _unversioned_parents(self, by_parent):
550
"""If parent directories are versioned, children must be versioned."""
552
for parent_id, children in by_parent.iteritems():
553
if parent_id is ROOT_PARENT:
555
if self.final_file_id(parent_id) is not None:
557
for child_id in children:
558
if self.final_file_id(child_id) is not None:
559
conflicts.append(('unversioned parent', parent_id))
563
def _improper_versioning(self):
564
"""Cannot version a file with no contents, or a bad type.
566
However, existing entries with no contents are okay.
569
for trans_id in self._new_id.iterkeys():
571
kind = self.final_kind(trans_id)
573
conflicts.append(('versioning no contents', trans_id))
575
if not InventoryEntry.versionable_kind(kind):
576
conflicts.append(('versioning bad kind', trans_id, kind))
579
def _executability_conflicts(self):
580
"""Check for bad executability changes.
582
Only versioned files may have their executability set, because
583
1. only versioned entries can have executability under windows
584
2. only files can be executable. (The execute bit on a directory
585
does not indicate searchability)
588
for trans_id in self._new_executability:
589
if self.final_file_id(trans_id) is None:
590
conflicts.append(('unversioned executability', trans_id))
593
non_file = self.final_kind(trans_id) != "file"
597
conflicts.append(('non-file executability', trans_id))
600
def _overwrite_conflicts(self):
601
"""Check for overwrites (not permitted on Win32)"""
603
for trans_id in self._new_contents:
605
self.tree_kind(trans_id)
608
if trans_id not in self._removed_contents:
609
conflicts.append(('overwrite', trans_id,
610
self.final_name(trans_id)))
613
def _duplicate_entries(self, by_parent):
614
"""No directory may have two entries with the same name."""
616
for children in by_parent.itervalues():
617
name_ids = [(self.final_name(t), t) for t in children]
621
for name, trans_id in name_ids:
622
if name == last_name:
623
conflicts.append(('duplicate', last_trans_id, trans_id,
626
kind = self.final_kind(trans_id)
629
file_id = self.final_file_id(trans_id)
630
if kind is not None or file_id is not None:
632
last_trans_id = trans_id
635
def _duplicate_ids(self):
636
"""Each inventory id may only be used once"""
638
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
640
active_tree_ids = set((f for f in self._tree.inventory if
641
f not in removed_tree_ids))
642
for trans_id, file_id in self._new_id.iteritems():
643
if file_id in active_tree_ids:
644
old_trans_id = self.trans_id_tree_file_id(file_id)
645
conflicts.append(('duplicate id', old_trans_id, trans_id))
648
def _parent_type_conflicts(self, by_parent):
649
"""parents must have directory 'contents'."""
651
for parent_id, children in by_parent.iteritems():
652
if parent_id is ROOT_PARENT:
654
if not self._any_contents(children):
656
for child in children:
658
self.final_kind(child)
662
kind = self.final_kind(parent_id)
666
conflicts.append(('missing parent', parent_id))
667
elif kind != "directory":
668
conflicts.append(('non-directory parent', parent_id))
671
def _any_contents(self, trans_ids):
672
"""Return true if any of the trans_ids, will have contents."""
673
for trans_id in trans_ids:
675
kind = self.final_kind(trans_id)
682
"""Apply all changes to the inventory and filesystem.
684
If filesystem or inventory conflicts are present, MalformedTransform
687
conflicts = self.find_conflicts()
688
if len(conflicts) != 0:
689
raise MalformedTransform(conflicts=conflicts)
691
inv = self._tree.inventory
692
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
694
child_pb.update('Apply phase', 0, 2)
695
self._apply_removals(inv, limbo_inv)
696
child_pb.update('Apply phase', 1, 2)
697
modified_paths = self._apply_insertions(inv, limbo_inv)
700
self._tree._write_inventory(inv)
703
return _TransformResults(modified_paths)
705
def _limbo_name(self, trans_id):
706
"""Generate the limbo name of a file"""
707
return pathjoin(self._limbodir, trans_id)
709
def _apply_removals(self, inv, limbo_inv):
710
"""Perform tree operations that remove directory/inventory names.
712
That is, delete files that are to be deleted, and put any files that
713
need renaming into limbo. This must be done in strict child-to-parent
716
tree_paths = list(self._tree_path_ids.iteritems())
717
tree_paths.sort(reverse=True)
718
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
720
for num, data in enumerate(tree_paths):
721
path, trans_id = data
722
child_pb.update('removing file', num, len(tree_paths))
723
full_path = self._tree.abspath(path)
724
if trans_id in self._removed_contents:
725
delete_any(full_path)
726
elif trans_id in self._new_name or trans_id in \
729
os.rename(full_path, self._limbo_name(trans_id))
731
if e.errno != errno.ENOENT:
733
if trans_id in self._removed_id:
734
if trans_id == self._new_root:
735
file_id = self._tree.inventory.root.file_id
737
file_id = self.tree_file_id(trans_id)
739
elif trans_id in self._new_name or trans_id in self._new_parent:
740
file_id = self.tree_file_id(trans_id)
741
if file_id is not None:
742
limbo_inv[trans_id] = inv[file_id]
747
def _apply_insertions(self, inv, limbo_inv):
748
"""Perform tree operations that insert directory/inventory names.
750
That is, create any files that need to be created, and restore from
751
limbo any files that needed renaming. This must be done in strict
752
parent-to-child order.
754
new_paths = self.new_paths()
756
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
758
for num, (path, trans_id) in enumerate(new_paths):
759
child_pb.update('adding file', num, len(new_paths))
761
kind = self._new_contents[trans_id]
763
kind = contents = None
764
if trans_id in self._new_contents or \
765
self.path_changed(trans_id):
766
full_path = self._tree.abspath(path)
768
os.rename(self._limbo_name(trans_id), full_path)
770
# We may be renaming a dangling inventory id
771
if e.errno != errno.ENOENT:
773
if trans_id in self._new_contents:
774
modified_paths.append(full_path)
775
del self._new_contents[trans_id]
777
if trans_id in self._new_id:
779
kind = file_kind(self._tree.abspath(path))
780
inv.add_path(path, kind, self._new_id[trans_id])
781
elif trans_id in self._new_name or trans_id in\
783
entry = limbo_inv.get(trans_id)
784
if entry is not None:
785
entry.name = self.final_name(trans_id)
786
parent_path = os.path.dirname(path)
788
self._tree.inventory.path2id(parent_path)
791
# requires files and inventory entries to be in place
792
if trans_id in self._new_executability:
793
self._set_executability(path, inv, trans_id)
796
return modified_paths
798
def _set_executability(self, path, inv, trans_id):
799
"""Set the executability of versioned files """
800
file_id = inv.path2id(path)
801
new_executability = self._new_executability[trans_id]
802
inv[file_id].executable = new_executability
803
if supports_executable():
804
abspath = self._tree.abspath(path)
805
current_mode = os.stat(abspath).st_mode
806
if new_executability:
809
to_mode = current_mode | (0100 & ~umask)
810
# Enable x-bit for others only if they can read it.
811
if current_mode & 0004:
812
to_mode |= 0001 & ~umask
813
if current_mode & 0040:
814
to_mode |= 0010 & ~umask
816
to_mode = current_mode & ~0111
817
os.chmod(abspath, to_mode)
819
def _new_entry(self, name, parent_id, file_id):
820
"""Helper function to create a new filesystem entry."""
821
trans_id = self.create_path(name, parent_id)
822
if file_id is not None:
823
self.version_file(file_id, trans_id)
826
def new_file(self, name, parent_id, contents, file_id=None,
828
"""Convenience method to create files.
830
name is the name of the file to create.
831
parent_id is the transaction id of the parent directory of the file.
832
contents is an iterator of bytestrings, which will be used to produce
834
file_id is the inventory ID of the file, if it is to be versioned.
836
trans_id = self._new_entry(name, parent_id, file_id)
837
self.create_file(contents, trans_id)
838
if executable is not None:
839
self.set_executability(executable, trans_id)
842
def new_directory(self, name, parent_id, file_id=None):
843
"""Convenience method to create directories.
845
name is the name of the directory to create.
846
parent_id is the transaction id of the parent directory of the
848
file_id is the inventory ID of the directory, if it is to be versioned.
850
trans_id = self._new_entry(name, parent_id, file_id)
851
self.create_directory(trans_id)
854
def new_symlink(self, name, parent_id, target, file_id=None):
855
"""Convenience method to create symbolic link.
857
name is the name of the symlink to create.
858
parent_id is the transaction id of the parent directory of the symlink.
859
target is a bytestring of the target of the symlink.
860
file_id is the inventory ID of the file, if it is to be versioned.
862
trans_id = self._new_entry(name, parent_id, file_id)
863
self.create_symlink(target, trans_id)
866
def joinpath(parent, child):
867
"""Join tree-relative paths, handling the tree root specially"""
868
if parent is None or parent == "":
871
return pathjoin(parent, child)
874
class FinalPaths(object):
875
"""Make path calculation cheap by memoizing paths.
877
The underlying tree must not be manipulated between calls, or else
878
the results will likely be incorrect.
880
def __init__(self, transform):
881
object.__init__(self)
882
self._known_paths = {}
883
self.transform = transform
885
def _determine_path(self, trans_id):
886
if trans_id == self.transform.root:
888
name = self.transform.final_name(trans_id)
889
parent_id = self.transform.final_parent(trans_id)
890
if parent_id == self.transform.root:
893
return pathjoin(self.get_path(parent_id), name)
895
def get_path(self, trans_id):
896
"""Find the final path associated with a trans_id"""
897
if trans_id not in self._known_paths:
898
self._known_paths[trans_id] = self._determine_path(trans_id)
899
return self._known_paths[trans_id]
901
def topology_sorted_ids(tree):
902
"""Determine the topological order of the ids in a tree"""
903
file_ids = list(tree)
904
file_ids.sort(key=tree.id2path)
907
def build_tree(tree, wt):
908
"""Create working tree for a branch, using a Transaction."""
910
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
911
pp = ProgressPhase("Build phase", 2, top_pb)
912
tt = TreeTransform(wt)
915
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
916
file_ids = topology_sorted_ids(tree)
917
pb = bzrlib.ui.ui_factory.nested_progress_bar()
919
for num, file_id in enumerate(file_ids):
920
pb.update("Building tree", num, len(file_ids))
921
entry = tree.inventory[file_id]
922
if entry.parent_id is None:
924
if entry.parent_id not in file_trans_id:
925
raise repr(entry.parent_id)
926
parent_id = file_trans_id[entry.parent_id]
927
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
937
def new_by_entry(tt, entry, parent_id, tree):
938
"""Create a new file according to its inventory entry"""
942
contents = tree.get_file(entry.file_id).readlines()
943
executable = tree.is_executable(entry.file_id)
944
return tt.new_file(name, parent_id, contents, entry.file_id,
946
elif kind == 'directory':
947
return tt.new_directory(name, parent_id, entry.file_id)
948
elif kind == 'symlink':
949
target = tree.get_symlink_target(entry.file_id)
950
return tt.new_symlink(name, parent_id, target, entry.file_id)
952
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
953
"""Create new file contents according to an inventory entry."""
954
if entry.kind == "file":
956
lines = tree.get_file(entry.file_id).readlines()
957
tt.create_file(lines, trans_id, mode_id=mode_id)
958
elif entry.kind == "symlink":
959
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
960
elif entry.kind == "directory":
961
tt.create_directory(trans_id)
963
def create_entry_executability(tt, entry, trans_id):
964
"""Set the executability of a trans_id according to an inventory entry"""
965
if entry.kind == "file":
966
tt.set_executability(entry.executable, trans_id)
969
def find_interesting(working_tree, target_tree, filenames):
970
"""Find the ids corresponding to specified filenames."""
972
interesting_ids = None
974
interesting_ids = set()
975
for tree_path in filenames:
977
for tree in (working_tree, target_tree):
978
file_id = tree.inventory.path2id(tree_path)
979
if file_id is not None:
980
interesting_ids.add(file_id)
983
raise NotVersionedError(path=tree_path)
984
return interesting_ids
987
def change_entry(tt, file_id, working_tree, target_tree,
988
trans_id_file_id, backups, trans_id, by_parent):
989
"""Replace a file_id's contents with those from a target tree."""
990
e_trans_id = trans_id_file_id(file_id)
991
entry = target_tree.inventory[file_id]
992
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
998
tt.delete_contents(e_trans_id)
1000
parent_trans_id = trans_id_file_id(entry.parent_id)
1001
backup_name = get_backup_name(entry, by_parent,
1002
parent_trans_id, tt)
1003
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1004
tt.unversion_file(e_trans_id)
1005
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1006
tt.version_file(file_id, e_trans_id)
1007
trans_id[file_id] = e_trans_id
1008
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1009
create_entry_executability(tt, entry, e_trans_id)
1012
tt.set_executability(entry.executable, e_trans_id)
1013
if tt.final_name(e_trans_id) != entry.name:
1016
parent_id = tt.final_parent(e_trans_id)
1017
parent_file_id = tt.final_file_id(parent_id)
1018
if parent_file_id != entry.parent_id:
1023
parent_trans_id = trans_id_file_id(entry.parent_id)
1024
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1027
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1028
"""Produce a backup-style name that appears to be available"""
1032
yield "%s.~%d~" % (entry.name, counter)
1034
for name in name_gen():
1035
if not tt.has_named_child(by_parent, parent_trans_id, name):
1038
def _entry_changes(file_id, entry, working_tree):
1039
"""Determine in which ways the inventory entry has changed.
1041
Returns booleans: has_contents, content_mod, meta_mod
1042
has_contents means there are currently contents, but they differ
1043
contents_mod means contents need to be modified
1044
meta_mod means the metadata needs to be modified
1046
cur_entry = working_tree.inventory[file_id]
1048
working_kind = working_tree.kind(file_id)
1051
if e.errno != errno.ENOENT:
1053
has_contents = False
1056
if has_contents is True:
1057
real_e_kind = entry.kind
1058
if real_e_kind == 'root_directory':
1059
real_e_kind = 'directory'
1060
if real_e_kind != working_kind:
1061
contents_mod, meta_mod = True, False
1063
cur_entry._read_tree_state(working_tree.id2path(file_id),
1065
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1066
cur_entry._forget_tree_state()
1067
return has_contents, contents_mod, meta_mod
1070
def revert(working_tree, target_tree, filenames, backups=False,
1071
pb=DummyProgress()):
1072
"""Revert a working tree's contents to those of a target tree."""
1073
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1074
def interesting(file_id):
1075
return interesting_ids is None or file_id in interesting_ids
1077
tt = TreeTransform(working_tree, pb)
1079
merge_modified = working_tree.merge_modified()
1081
def trans_id_file_id(file_id):
1083
return trans_id[file_id]
1085
return tt.trans_id_tree_file_id(file_id)
1087
pp = ProgressPhase("Revert phase", 4, pb)
1089
sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1091
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1093
by_parent = tt.by_parent()
1094
for id_num, file_id in enumerate(sorted_interesting):
1095
child_pb.update("Reverting file", id_num+1,
1096
len(sorted_interesting))
1097
if file_id not in working_tree.inventory:
1098
entry = target_tree.inventory[file_id]
1099
parent_id = trans_id_file_id(entry.parent_id)
1100
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1101
trans_id[file_id] = e_trans_id
1103
backup_this = backups
1104
if file_id in merge_modified:
1106
del merge_modified[file_id]
1107
change_entry(tt, file_id, working_tree, target_tree,
1108
trans_id_file_id, backup_this, trans_id,
1113
wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1114
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1116
for id_num, file_id in enumerate(wt_interesting):
1117
child_pb.update("New file check", id_num+1,
1118
len(sorted_interesting))
1119
if file_id not in target_tree:
1120
trans_id = tt.trans_id_tree_file_id(file_id)
1121
tt.unversion_file(trans_id)
1122
if file_id in merge_modified:
1123
tt.delete_contents(trans_id)
1124
del merge_modified[file_id]
1128
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1130
raw_conflicts = resolve_conflicts(tt, child_pb)
1133
conflicts = cook_conflicts(raw_conflicts, tt)
1134
for conflict in conflicts:
1138
working_tree.set_merge_modified({})
1145
def resolve_conflicts(tt, pb=DummyProgress()):
1146
"""Make many conflict-resolution attempts, but die if they fail"""
1147
new_conflicts = set()
1150
pb.update('Resolution pass', n+1, 10)
1151
conflicts = tt.find_conflicts()
1152
if len(conflicts) == 0:
1153
return new_conflicts
1154
new_conflicts.update(conflict_pass(tt, conflicts))
1155
raise MalformedTransform(conflicts=conflicts)
1160
def conflict_pass(tt, conflicts):
1161
"""Resolve some classes of conflicts."""
1162
new_conflicts = set()
1163
for c_type, conflict in ((c[0], c) for c in conflicts):
1164
if c_type == 'duplicate id':
1165
tt.unversion_file(conflict[1])
1166
new_conflicts.add((c_type, 'Unversioned existing file',
1167
conflict[1], conflict[2], ))
1168
elif c_type == 'duplicate':
1169
# files that were renamed take precedence
1170
new_name = tt.final_name(conflict[1])+'.moved'
1171
final_parent = tt.final_parent(conflict[1])
1172
if tt.path_changed(conflict[1]):
1173
tt.adjust_path(new_name, final_parent, conflict[2])
1174
new_conflicts.add((c_type, 'Moved existing file to',
1175
conflict[2], conflict[1]))
1177
tt.adjust_path(new_name, final_parent, conflict[1])
1178
new_conflicts.add((c_type, 'Moved existing file to',
1179
conflict[1], conflict[2]))
1180
elif c_type == 'parent loop':
1181
# break the loop by undoing one of the ops that caused the loop
1183
while not tt.path_changed(cur):
1184
cur = tt.final_parent(cur)
1185
new_conflicts.add((c_type, 'Cancelled move', cur,
1186
tt.final_parent(cur),))
1187
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1189
elif c_type == 'missing parent':
1190
trans_id = conflict[1]
1192
tt.cancel_deletion(trans_id)
1193
new_conflicts.add((c_type, 'Not deleting', trans_id))
1195
tt.create_directory(trans_id)
1196
new_conflicts.add((c_type, 'Created directory.', trans_id))
1197
elif c_type == 'unversioned parent':
1198
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1199
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1200
return new_conflicts
1203
def cook_conflicts(raw_conflicts, tt):
1204
"""Generate a list of cooked conflicts, sorted by file path"""
1205
from bzrlib.conflicts import Conflict
1206
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1207
return sorted(conflict_iter, key=Conflict.sort_key)
1210
def iter_cook_conflicts(raw_conflicts, tt):
1211
from bzrlib.conflicts import Conflict
1213
for conflict in raw_conflicts:
1214
c_type = conflict[0]
1215
action = conflict[1]
1216
modified_path = fp.get_path(conflict[2])
1217
modified_id = tt.final_file_id(conflict[2])
1218
if len(conflict) == 3:
1219
yield Conflict.factory(c_type, action=action, path=modified_path,
1220
file_id=modified_id)
1223
conflicting_path = fp.get_path(conflict[3])
1224
conflicting_id = tt.final_file_id(conflict[3])
1225
yield Conflict.factory(c_type, action=action, path=modified_path,
1226
file_id=modified_id,
1227
conflict_path=conflicting_path,
1228
conflict_file_id=conflicting_id)