1
# Copyright (C) 2006, 2007 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.lazy_import import lazy_import
22
lazy_import(globals(), """
30
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
31
ReusingTransform, NotVersionedError, CantMoveRoot,
32
ExistingLimbo, ImmortalLimbo, NoFinalPath)
33
from bzrlib.inventory import InventoryEntry
34
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
36
from bzrlib.progress import DummyProgress, ProgressPhase
37
from bzrlib.trace import mutter, warning
38
from bzrlib import tree
40
import bzrlib.urlutils as urlutils
43
ROOT_PARENT = "root-parent"
46
def unique_add(map, key, value):
48
raise DuplicateKey(key=key)
52
class _TransformResults(object):
53
def __init__(self, modified_paths):
55
self.modified_paths = modified_paths
58
class TreeTransform(object):
59
"""Represent a tree transformation.
61
This object is designed to support incremental generation of the transform,
64
It is easy to produce malformed transforms, but they are generally
65
harmless. Attempting to apply a malformed transform will cause an
66
exception to be raised before any modifications are made to the tree.
68
Many kinds of malformed transforms can be corrected with the
69
resolve_conflicts function. The remaining ones indicate programming error,
70
such as trying to create a file with no path.
72
Two sets of file creation methods are supplied. Convenience methods are:
77
These are composed of the low-level methods:
79
* create_file or create_directory or create_symlink
83
def __init__(self, tree, pb=DummyProgress()):
84
"""Note: a tree_write lock is taken on the tree.
86
Use TreeTransform.finalize() to release the lock
90
self._tree.lock_tree_write()
92
control_files = self._tree._control_files
93
self._limbodir = urlutils.local_path_from_url(
94
control_files.controlfilename('limbo'))
96
os.mkdir(self._limbodir)
98
if e.errno == errno.EEXIST:
99
raise ExistingLimbo(self._limbodir)
106
self._new_parent = {}
107
self._new_contents = {}
108
self._removed_contents = set()
109
self._new_executability = {}
110
self._new_reference_revision = {}
112
self._non_present_ids = {}
114
self._removed_id = set()
115
self._tree_path_ids = {}
116
self._tree_id_paths = {}
118
# Cache of realpath results, to speed up canonical_path
120
# Cache of relpath results, to speed up canonical_path
121
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
125
def __get_root(self):
126
return self._new_root
128
root = property(__get_root)
131
"""Release the working tree lock, if held, clean up limbo dir."""
132
if self._tree is None:
135
for trans_id, kind in self._new_contents.iteritems():
136
path = self._limbo_name(trans_id)
137
if kind == "directory":
142
os.rmdir(self._limbodir)
144
# We don't especially care *why* the dir is immortal.
145
raise ImmortalLimbo(self._limbodir)
150
def _assign_id(self):
151
"""Produce a new tranform id"""
152
new_id = "new-%s" % self._id_number
156
def create_path(self, name, parent):
157
"""Assign a transaction id to a new path"""
158
trans_id = self._assign_id()
159
unique_add(self._new_name, trans_id, name)
160
unique_add(self._new_parent, trans_id, parent)
163
def adjust_path(self, name, parent, trans_id):
164
"""Change the path that is assigned to a transaction id."""
165
if trans_id == self._new_root:
167
self._new_name[trans_id] = name
168
self._new_parent[trans_id] = parent
170
def adjust_root_path(self, name, parent):
171
"""Emulate moving the root by moving all children, instead.
173
We do this by undoing the association of root's transaction id with the
174
current tree. This allows us to create a new directory with that
175
transaction id. We unversion the root directory and version the
176
physically new directory, and hope someone versions the tree root
179
old_root = self._new_root
180
old_root_file_id = self.final_file_id(old_root)
181
# force moving all children of root
182
for child_id in self.iter_tree_children(old_root):
183
if child_id != parent:
184
self.adjust_path(self.final_name(child_id),
185
self.final_parent(child_id), child_id)
186
file_id = self.final_file_id(child_id)
187
if file_id is not None:
188
self.unversion_file(child_id)
189
self.version_file(file_id, child_id)
191
# the physical root needs a new transaction id
192
self._tree_path_ids.pop("")
193
self._tree_id_paths.pop(old_root)
194
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
195
if parent == old_root:
196
parent = self._new_root
197
self.adjust_path(name, parent, old_root)
198
self.create_directory(old_root)
199
self.version_file(old_root_file_id, old_root)
200
self.unversion_file(self._new_root)
202
def trans_id_tree_file_id(self, inventory_id):
203
"""Determine the transaction id of a working tree file.
205
This reflects only files that already exist, not ones that will be
206
added by transactions.
208
path = self._tree.inventory.id2path(inventory_id)
209
return self.trans_id_tree_path(path)
211
def trans_id_file_id(self, file_id):
212
"""Determine or set the transaction id associated with a file ID.
213
A new id is only created for file_ids that were never present. If
214
a transaction has been unversioned, it is deliberately still returned.
215
(this will likely lead to an unversioned parent conflict.)
217
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
218
return self._r_new_id[file_id]
219
elif file_id in self._tree.inventory:
220
return self.trans_id_tree_file_id(file_id)
221
elif file_id in self._non_present_ids:
222
return self._non_present_ids[file_id]
224
trans_id = self._assign_id()
225
self._non_present_ids[file_id] = trans_id
228
def canonical_path(self, path):
229
"""Get the canonical tree-relative path"""
230
# don't follow final symlinks
231
abs = self._tree.abspath(path)
232
if abs in self._relpaths:
233
return self._relpaths[abs]
234
dirname, basename = os.path.split(abs)
235
if dirname not in self._realpaths:
236
self._realpaths[dirname] = os.path.realpath(dirname)
237
dirname = self._realpaths[dirname]
238
abs = pathjoin(dirname, basename)
239
if dirname in self._relpaths:
240
relpath = pathjoin(self._relpaths[dirname], basename)
241
relpath = relpath.rstrip('/\\')
243
relpath = self._tree.relpath(abs)
244
self._relpaths[abs] = relpath
247
def trans_id_tree_path(self, path):
248
"""Determine (and maybe set) the transaction ID for a tree path."""
249
path = self.canonical_path(path)
250
if path not in self._tree_path_ids:
251
self._tree_path_ids[path] = self._assign_id()
252
self._tree_id_paths[self._tree_path_ids[path]] = path
253
return self._tree_path_ids[path]
255
def get_tree_parent(self, trans_id):
256
"""Determine id of the parent in the tree."""
257
path = self._tree_id_paths[trans_id]
260
return self.trans_id_tree_path(os.path.dirname(path))
262
def create_file(self, contents, trans_id, mode_id=None):
263
"""Schedule creation of a new file.
267
Contents is an iterator of strings, all of which will be written
268
to the target destination.
270
New file takes the permissions of any existing file with that id,
271
unless mode_id is specified.
273
name = self._limbo_name(trans_id)
277
unique_add(self._new_contents, trans_id, 'file')
279
# Clean up the file, it never got registered so
280
# TreeTransform.finalize() won't clean it up.
285
f.writelines(contents)
288
self._set_mode(trans_id, mode_id, S_ISREG)
290
def _set_mode(self, trans_id, mode_id, typefunc):
291
"""Set the mode of new file contents.
292
The mode_id is the existing file to get the mode from (often the same
293
as trans_id). The operation is only performed if there's a mode match
294
according to typefunc.
299
old_path = self._tree_id_paths[mode_id]
303
mode = os.stat(self._tree.abspath(old_path)).st_mode
305
if e.errno == errno.ENOENT:
310
os.chmod(self._limbo_name(trans_id), mode)
312
def create_directory(self, trans_id):
313
"""Schedule creation of a new directory.
315
See also new_directory.
317
os.mkdir(self._limbo_name(trans_id))
318
unique_add(self._new_contents, trans_id, 'directory')
320
def create_symlink(self, target, trans_id):
321
"""Schedule creation of a new symbolic link.
323
target is a bytestring.
324
See also new_symlink.
326
os.symlink(target, self._limbo_name(trans_id))
327
unique_add(self._new_contents, trans_id, 'symlink')
329
def cancel_creation(self, trans_id):
330
"""Cancel the creation of new file contents."""
331
del self._new_contents[trans_id]
332
delete_any(self._limbo_name(trans_id))
334
def delete_contents(self, trans_id):
335
"""Schedule the contents of a path entry for deletion"""
336
self.tree_kind(trans_id)
337
self._removed_contents.add(trans_id)
339
def cancel_deletion(self, trans_id):
340
"""Cancel a scheduled deletion"""
341
self._removed_contents.remove(trans_id)
343
def unversion_file(self, trans_id):
344
"""Schedule a path entry to become unversioned"""
345
self._removed_id.add(trans_id)
347
def delete_versioned(self, trans_id):
348
"""Delete and unversion a versioned file"""
349
self.delete_contents(trans_id)
350
self.unversion_file(trans_id)
352
def set_executability(self, executability, trans_id):
353
"""Schedule setting of the 'execute' bit
354
To unschedule, set to None
356
if executability is None:
357
del self._new_executability[trans_id]
359
unique_add(self._new_executability, trans_id, executability)
361
def set_tree_reference(self, revision_id, trans_id):
362
"""Set the reference associated with a directory"""
363
unique_add(self._new_reference_revision, trans_id, revision_id)
365
def version_file(self, file_id, trans_id):
366
"""Schedule a file to become versioned."""
367
assert file_id is not None
368
unique_add(self._new_id, trans_id, file_id)
369
unique_add(self._r_new_id, file_id, trans_id)
371
def cancel_versioning(self, trans_id):
372
"""Undo a previous versioning of a file"""
373
file_id = self._new_id[trans_id]
374
del self._new_id[trans_id]
375
del self._r_new_id[file_id]
378
"""Determine the paths of all new and changed files"""
380
fp = FinalPaths(self)
381
for id_set in (self._new_name, self._new_parent, self._new_contents,
382
self._new_id, self._new_executability):
383
new_ids.update(id_set)
384
new_paths = [(fp.get_path(t), t) for t in new_ids]
388
def tree_kind(self, trans_id):
389
"""Determine the file kind in the working tree.
391
Raises NoSuchFile if the file does not exist
393
path = self._tree_id_paths.get(trans_id)
395
raise NoSuchFile(None)
397
return file_kind(self._tree.abspath(path))
399
if e.errno != errno.ENOENT:
402
raise NoSuchFile(path)
404
def final_kind(self, trans_id):
405
"""Determine the final file kind, after any changes applied.
407
Raises NoSuchFile if the file does not exist/has no contents.
408
(It is conceivable that a path would be created without the
409
corresponding contents insertion command)
411
if trans_id in self._new_contents:
412
return self._new_contents[trans_id]
413
elif trans_id in self._removed_contents:
414
raise NoSuchFile(None)
416
return self.tree_kind(trans_id)
418
def tree_file_id(self, trans_id):
419
"""Determine the file id associated with the trans_id in the tree"""
421
path = self._tree_id_paths[trans_id]
423
# the file is a new, unversioned file, or invalid trans_id
425
# the file is old; the old id is still valid
426
if self._new_root == trans_id:
427
return self._tree.inventory.root.file_id
428
return self._tree.inventory.path2id(path)
430
def final_file_id(self, trans_id):
431
"""Determine the file id after any changes are applied, or None.
433
None indicates that the file will not be versioned after changes are
437
# there is a new id for this file
438
assert self._new_id[trans_id] is not None
439
return self._new_id[trans_id]
441
if trans_id in self._removed_id:
443
return self.tree_file_id(trans_id)
445
def inactive_file_id(self, trans_id):
446
"""Return the inactive file_id associated with a transaction id.
447
That is, the one in the tree or in non_present_ids.
448
The file_id may actually be active, too.
450
file_id = self.tree_file_id(trans_id)
451
if file_id is not None:
453
for key, value in self._non_present_ids.iteritems():
454
if value == trans_id:
457
def final_parent(self, trans_id):
458
"""Determine the parent file_id, after any changes are applied.
460
ROOT_PARENT is returned for the tree root.
463
return self._new_parent[trans_id]
465
return self.get_tree_parent(trans_id)
467
def final_name(self, trans_id):
468
"""Determine the final filename, after all changes are applied."""
470
return self._new_name[trans_id]
473
return os.path.basename(self._tree_id_paths[trans_id])
475
raise NoFinalPath(trans_id, self)
478
"""Return a map of parent: children for known parents.
480
Only new paths and parents of tree files with assigned ids are used.
483
items = list(self._new_parent.iteritems())
484
items.extend((t, self.final_parent(t)) for t in
485
self._tree_id_paths.keys())
486
for trans_id, parent_id in items:
487
if parent_id not in by_parent:
488
by_parent[parent_id] = set()
489
by_parent[parent_id].add(trans_id)
492
def path_changed(self, trans_id):
493
"""Return True if a trans_id's path has changed."""
494
return (trans_id in self._new_name) or (trans_id in self._new_parent)
496
def new_contents(self, trans_id):
497
return (trans_id in self._new_contents)
499
def find_conflicts(self):
500
"""Find any violations of inventory or filesystem invariants"""
501
if self.__done is True:
502
raise ReusingTransform()
504
# ensure all children of all existent parents are known
505
# all children of non-existent parents are known, by definition.
506
self._add_tree_children()
507
by_parent = self.by_parent()
508
conflicts.extend(self._unversioned_parents(by_parent))
509
conflicts.extend(self._parent_loops())
510
conflicts.extend(self._duplicate_entries(by_parent))
511
conflicts.extend(self._duplicate_ids())
512
conflicts.extend(self._parent_type_conflicts(by_parent))
513
conflicts.extend(self._improper_versioning())
514
conflicts.extend(self._executability_conflicts())
515
conflicts.extend(self._overwrite_conflicts())
518
def _add_tree_children(self):
519
"""Add all the children of all active parents to the known paths.
521
Active parents are those which gain children, and those which are
522
removed. This is a necessary first step in detecting conflicts.
524
parents = self.by_parent().keys()
525
parents.extend([t for t in self._removed_contents if
526
self.tree_kind(t) == 'directory'])
527
for trans_id in self._removed_id:
528
file_id = self.tree_file_id(trans_id)
529
if self._tree.inventory[file_id].kind == 'directory':
530
parents.append(trans_id)
532
for parent_id in parents:
533
# ensure that all children are registered with the transaction
534
list(self.iter_tree_children(parent_id))
536
def iter_tree_children(self, parent_id):
537
"""Iterate through the entry's tree children, if any"""
539
path = self._tree_id_paths[parent_id]
543
children = os.listdir(self._tree.abspath(path))
545
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
549
for child in children:
550
childpath = joinpath(path, child)
551
if self._tree.is_control_filename(childpath):
553
yield self.trans_id_tree_path(childpath)
555
def has_named_child(self, by_parent, parent_id, name):
557
children = by_parent[parent_id]
560
for child in children:
561
if self.final_name(child) == name:
564
path = self._tree_id_paths[parent_id]
567
childpath = joinpath(path, name)
568
child_id = self._tree_path_ids.get(childpath)
570
return lexists(self._tree.abspath(childpath))
572
if self.final_parent(child_id) != parent_id:
574
if child_id in self._removed_contents:
575
# XXX What about dangling file-ids?
580
def _parent_loops(self):
581
"""No entry should be its own ancestor"""
583
for trans_id in self._new_parent:
586
while parent_id is not ROOT_PARENT:
589
parent_id = self.final_parent(parent_id)
592
if parent_id == trans_id:
593
conflicts.append(('parent loop', trans_id))
594
if parent_id in seen:
598
def _unversioned_parents(self, by_parent):
599
"""If parent directories are versioned, children must be versioned."""
601
for parent_id, children in by_parent.iteritems():
602
if parent_id is ROOT_PARENT:
604
if self.final_file_id(parent_id) is not None:
606
for child_id in children:
607
if self.final_file_id(child_id) is not None:
608
conflicts.append(('unversioned parent', parent_id))
612
def _improper_versioning(self):
613
"""Cannot version a file with no contents, or a bad type.
615
However, existing entries with no contents are okay.
618
for trans_id in self._new_id.iterkeys():
620
kind = self.final_kind(trans_id)
622
conflicts.append(('versioning no contents', trans_id))
624
if not InventoryEntry.versionable_kind(kind):
625
conflicts.append(('versioning bad kind', trans_id, kind))
628
def _executability_conflicts(self):
629
"""Check for bad executability changes.
631
Only versioned files may have their executability set, because
632
1. only versioned entries can have executability under windows
633
2. only files can be executable. (The execute bit on a directory
634
does not indicate searchability)
637
for trans_id in self._new_executability:
638
if self.final_file_id(trans_id) is None:
639
conflicts.append(('unversioned executability', trans_id))
642
non_file = self.final_kind(trans_id) != "file"
646
conflicts.append(('non-file executability', trans_id))
649
def _overwrite_conflicts(self):
650
"""Check for overwrites (not permitted on Win32)"""
652
for trans_id in self._new_contents:
654
self.tree_kind(trans_id)
657
if trans_id not in self._removed_contents:
658
conflicts.append(('overwrite', trans_id,
659
self.final_name(trans_id)))
662
def _duplicate_entries(self, by_parent):
663
"""No directory may have two entries with the same name."""
665
for children in by_parent.itervalues():
666
name_ids = [(self.final_name(t), t) for t in children]
670
for name, trans_id in name_ids:
672
kind = self.final_kind(trans_id)
675
file_id = self.final_file_id(trans_id)
676
if kind is None and file_id is None:
678
if name == last_name:
679
conflicts.append(('duplicate', last_trans_id, trans_id,
682
last_trans_id = trans_id
685
def _duplicate_ids(self):
686
"""Each inventory id may only be used once"""
688
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
690
active_tree_ids = set((f for f in self._tree.inventory if
691
f not in removed_tree_ids))
692
for trans_id, file_id in self._new_id.iteritems():
693
if file_id in active_tree_ids:
694
old_trans_id = self.trans_id_tree_file_id(file_id)
695
conflicts.append(('duplicate id', old_trans_id, trans_id))
698
def _parent_type_conflicts(self, by_parent):
699
"""parents must have directory 'contents'."""
701
for parent_id, children in by_parent.iteritems():
702
if parent_id is ROOT_PARENT:
704
if not self._any_contents(children):
706
for child in children:
708
self.final_kind(child)
712
kind = self.final_kind(parent_id)
716
conflicts.append(('missing parent', parent_id))
717
elif kind != "directory":
718
conflicts.append(('non-directory parent', parent_id))
721
def _any_contents(self, trans_ids):
722
"""Return true if any of the trans_ids, will have contents."""
723
for trans_id in trans_ids:
725
kind = self.final_kind(trans_id)
732
"""Apply all changes to the inventory and filesystem.
734
If filesystem or inventory conflicts are present, MalformedTransform
737
conflicts = self.find_conflicts()
738
if len(conflicts) != 0:
739
raise MalformedTransform(conflicts=conflicts)
741
inv = self._tree.inventory
742
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
744
child_pb.update('Apply phase', 0, 2)
745
self._apply_removals(inv, limbo_inv)
746
child_pb.update('Apply phase', 1, 2)
747
modified_paths = self._apply_insertions(inv, limbo_inv)
750
self._tree._write_inventory(inv)
753
return _TransformResults(modified_paths)
755
def _limbo_name(self, trans_id):
756
"""Generate the limbo name of a file"""
757
return pathjoin(self._limbodir, trans_id)
759
def _apply_removals(self, inv, limbo_inv):
760
"""Perform tree operations that remove directory/inventory names.
762
That is, delete files that are to be deleted, and put any files that
763
need renaming into limbo. This must be done in strict child-to-parent
766
tree_paths = list(self._tree_path_ids.iteritems())
767
tree_paths.sort(reverse=True)
768
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
770
for num, data in enumerate(tree_paths):
771
path, trans_id = data
772
child_pb.update('removing file', num, len(tree_paths))
773
full_path = self._tree.abspath(path)
774
if trans_id in self._removed_contents:
775
delete_any(full_path)
776
elif trans_id in self._new_name or trans_id in \
779
os.rename(full_path, self._limbo_name(trans_id))
781
if e.errno != errno.ENOENT:
783
if trans_id in self._removed_id:
784
if trans_id == self._new_root:
785
file_id = self._tree.inventory.root.file_id
787
file_id = self.tree_file_id(trans_id)
789
elif trans_id in self._new_name or trans_id in self._new_parent:
790
file_id = self.tree_file_id(trans_id)
791
if file_id is not None:
792
limbo_inv[trans_id] = inv[file_id]
793
inv.remove_recursive_id(file_id)
797
def _apply_insertions(self, inv, limbo_inv):
798
"""Perform tree operations that insert directory/inventory names.
800
That is, create any files that need to be created, and restore from
801
limbo any files that needed renaming. This must be done in strict
802
parent-to-child order.
804
new_paths = self.new_paths()
806
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
808
for num, (path, trans_id) in enumerate(new_paths):
809
child_pb.update('adding file', num, len(new_paths))
811
kind = self._new_contents[trans_id]
813
kind = contents = None
814
if trans_id in self._new_contents or \
815
self.path_changed(trans_id):
816
full_path = self._tree.abspath(path)
818
os.rename(self._limbo_name(trans_id), full_path)
820
# We may be renaming a dangling inventory id
821
if e.errno != errno.ENOENT:
823
if trans_id in self._new_contents:
824
modified_paths.append(full_path)
825
del self._new_contents[trans_id]
827
if trans_id in self._new_id:
829
kind = file_kind(self._tree.abspath(path))
830
if trans_id in self._new_reference_revision:
831
entry = inventory.TreeReference(self._new_id[trans_id],
832
self._new_name[trans_id],
833
self.final_file_id(self._new_parent[trans_id]),
834
None, self._new_reference_revision[trans_id])
837
inv.add_path(path, kind, self._new_id[trans_id])
838
elif trans_id in self._new_name or trans_id in\
840
entry = limbo_inv.get(trans_id)
841
if entry is not None:
842
entry.name = self.final_name(trans_id)
843
parent_path = os.path.dirname(path)
845
self._tree.inventory.path2id(parent_path)
848
# requires files and inventory entries to be in place
849
if trans_id in self._new_executability:
850
self._set_executability(path, inv, trans_id)
853
return modified_paths
855
def _set_executability(self, path, inv, trans_id):
856
"""Set the executability of versioned files """
857
file_id = inv.path2id(path)
858
new_executability = self._new_executability[trans_id]
859
inv[file_id].executable = new_executability
860
if supports_executable():
861
abspath = self._tree.abspath(path)
862
current_mode = os.stat(abspath).st_mode
863
if new_executability:
866
to_mode = current_mode | (0100 & ~umask)
867
# Enable x-bit for others only if they can read it.
868
if current_mode & 0004:
869
to_mode |= 0001 & ~umask
870
if current_mode & 0040:
871
to_mode |= 0010 & ~umask
873
to_mode = current_mode & ~0111
874
os.chmod(abspath, to_mode)
876
def _new_entry(self, name, parent_id, file_id):
877
"""Helper function to create a new filesystem entry."""
878
trans_id = self.create_path(name, parent_id)
879
if file_id is not None:
880
self.version_file(file_id, trans_id)
883
def new_file(self, name, parent_id, contents, file_id=None,
885
"""Convenience method to create files.
887
name is the name of the file to create.
888
parent_id is the transaction id of the parent directory of the file.
889
contents is an iterator of bytestrings, which will be used to produce
891
:param file_id: The inventory ID of the file, if it is to be versioned.
892
:param executable: Only valid when a file_id has been supplied.
894
trans_id = self._new_entry(name, parent_id, file_id)
895
# TODO: rather than scheduling a set_executable call,
896
# have create_file create the file with the right mode.
897
self.create_file(contents, trans_id)
898
if executable is not None:
899
self.set_executability(executable, trans_id)
902
def new_directory(self, name, parent_id, file_id=None):
903
"""Convenience method to create directories.
905
name is the name of the directory to create.
906
parent_id is the transaction id of the parent directory of the
908
file_id is the inventory ID of the directory, if it is to be versioned.
910
trans_id = self._new_entry(name, parent_id, file_id)
911
self.create_directory(trans_id)
914
def new_symlink(self, name, parent_id, target, file_id=None):
915
"""Convenience method to create symbolic link.
917
name is the name of the symlink to create.
918
parent_id is the transaction id of the parent directory of the symlink.
919
target is a bytestring of the target of the symlink.
920
file_id is the inventory ID of the file, if it is to be versioned.
922
trans_id = self._new_entry(name, parent_id, file_id)
923
self.create_symlink(target, trans_id)
926
def _affected_ids(self):
927
"""Return the set of transform ids affected by the transform"""
928
trans_ids = set(self._removed_id)
929
trans_ids.update(self._new_id.keys())
930
trans_ids.update(self._removed_contents)
931
trans_ids.update(self._new_contents.keys())
932
trans_ids.update(self._new_executability.keys())
933
trans_ids.update(self._new_name.keys())
934
trans_ids.update(self._new_parent.keys())
937
def _get_file_id_maps(self):
938
"""Return mapping of file_ids to trans_ids in the to and from states"""
939
trans_ids = self._affected_ids()
942
# Build up two dicts: trans_ids associated with file ids in the
943
# FROM state, vs the TO state.
944
for trans_id in trans_ids:
945
from_file_id = self.tree_file_id(trans_id)
946
if from_file_id is not None:
947
from_trans_ids[from_file_id] = trans_id
948
to_file_id = self.final_file_id(trans_id)
949
if to_file_id is not None:
950
to_trans_ids[to_file_id] = trans_id
951
return from_trans_ids, to_trans_ids
953
def _from_file_data(self, from_trans_id, from_versioned, file_id):
954
"""Get data about a file in the from (tree) state
956
Return a (name, parent, kind, executable) tuple
958
from_path = self._tree_id_paths.get(from_trans_id)
960
# get data from working tree if versioned
961
from_entry = self._tree.inventory[file_id]
962
from_name = from_entry.name
963
from_parent = from_entry.parent_id
966
if from_path is None:
967
# File does not exist in FROM state
971
# File exists, but is not versioned. Have to use path-
973
from_name = os.path.basename(from_path)
974
tree_parent = self.get_tree_parent(from_trans_id)
975
from_parent = self.tree_file_id(tree_parent)
976
if from_path is not None:
977
from_kind, from_executable, from_stats = \
978
self._tree._comparison_data(from_entry, from_path)
981
from_executable = False
982
return from_name, from_parent, from_kind, from_executable
984
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
985
"""Get data about a file in the to (target) state
987
Return a (name, parent, kind, executable) tuple
989
to_name = self.final_name(to_trans_id)
991
to_kind = self.final_kind(to_trans_id)
994
to_parent = self.final_file_id(self.final_parent(to_trans_id))
995
if to_trans_id in self._new_executability:
996
to_executable = self._new_executability[to_trans_id]
997
elif to_trans_id == from_trans_id:
998
to_executable = from_executable
1000
to_executable = False
1001
return to_name, to_parent, to_kind, to_executable
1003
def _iter_changes(self):
1004
"""Produce output in the same format as Tree._iter_changes.
1006
Will produce nonsensical results if invoked while inventory/filesystem
1007
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1009
This reads the Transform, but only reproduces changes involving a
1010
file_id. Files that are not versioned in either of the FROM or TO
1011
states are not reflected.
1013
final_paths = FinalPaths(self)
1014
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1016
# Now iterate through all active file_ids
1017
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1019
from_trans_id = from_trans_ids.get(file_id)
1020
# find file ids, and determine versioning state
1021
if from_trans_id is None:
1022
from_versioned = False
1023
from_trans_id = to_trans_ids[file_id]
1025
from_versioned = True
1026
to_trans_id = to_trans_ids.get(file_id)
1027
if to_trans_id is None:
1028
to_versioned = False
1029
to_trans_id = from_trans_id
1033
from_name, from_parent, from_kind, from_executable = \
1034
self._from_file_data(from_trans_id, from_versioned, file_id)
1036
to_name, to_parent, to_kind, to_executable = \
1037
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1039
to_path = final_paths.get_path(to_trans_id)
1040
if from_kind != to_kind:
1042
elif to_kind in ('file' or 'symlink') and (
1043
to_trans_id != from_trans_id or
1044
to_trans_id in self._new_contents):
1046
if (not modified and from_versioned == to_versioned and
1047
from_parent==to_parent and from_name == to_name and
1048
from_executable == to_executable):
1050
results.append((file_id, to_path, modified,
1051
(from_versioned, to_versioned),
1052
(from_parent, to_parent),
1053
(from_name, to_name),
1054
(from_kind, to_kind),
1055
(from_executable, to_executable)))
1056
return iter(sorted(results, key=lambda x:x[1]))
1059
def joinpath(parent, child):
1060
"""Join tree-relative paths, handling the tree root specially"""
1061
if parent is None or parent == "":
1064
return pathjoin(parent, child)
1067
class FinalPaths(object):
1068
"""Make path calculation cheap by memoizing paths.
1070
The underlying tree must not be manipulated between calls, or else
1071
the results will likely be incorrect.
1073
def __init__(self, transform):
1074
object.__init__(self)
1075
self._known_paths = {}
1076
self.transform = transform
1078
def _determine_path(self, trans_id):
1079
if trans_id == self.transform.root:
1081
name = self.transform.final_name(trans_id)
1082
parent_id = self.transform.final_parent(trans_id)
1083
if parent_id == self.transform.root:
1086
return pathjoin(self.get_path(parent_id), name)
1088
def get_path(self, trans_id):
1089
"""Find the final path associated with a trans_id"""
1090
if trans_id not in self._known_paths:
1091
self._known_paths[trans_id] = self._determine_path(trans_id)
1092
return self._known_paths[trans_id]
1094
def topology_sorted_ids(tree):
1095
"""Determine the topological order of the ids in a tree"""
1096
file_ids = list(tree)
1097
file_ids.sort(key=tree.id2path)
1101
def build_tree(tree, wt):
1102
"""Create working tree for a branch, using a TreeTransform.
1104
This function should be used on empty trees, having a tree root at most.
1105
(see merge and revert functionality for working with existing trees)
1107
Existing files are handled like so:
1109
- Existing bzrdirs take precedence over creating new items. They are
1110
created as '%s.diverted' % name.
1111
- Otherwise, if the content on disk matches the content we are building,
1112
it is silently replaced.
1113
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1115
if len(wt.inventory) > 1: # more than just a root
1116
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1118
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1119
pp = ProgressPhase("Build phase", 2, top_pb)
1120
# if tree.inventory.root is not None:
1121
# wt.set_root_id(tree.inventory.root.file_id)
1122
tt = TreeTransform(wt)
1126
file_trans_id[wt.get_root_id()] = \
1127
tt.trans_id_tree_file_id(wt.get_root_id())
1128
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1130
for num, (tree_path, entry) in \
1131
enumerate(tree.inventory.iter_entries_by_dir()):
1132
pb.update("Building tree", num, len(tree.inventory))
1133
if entry.parent_id is None:
1136
file_id = entry.file_id
1137
target_path = wt.abspath(tree_path)
1139
kind = file_kind(target_path)
1143
if kind == "directory":
1145
bzrdir.BzrDir.open(target_path)
1146
except errors.NotBranchError:
1150
if (file_id not in divert and
1151
_content_match(tree, entry, file_id, kind,
1153
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1154
if kind == 'directory':
1156
if entry.parent_id not in file_trans_id:
1157
raise repr(entry.parent_id)
1158
parent_id = file_trans_id[entry.parent_id]
1159
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1162
new_trans_id = file_trans_id[file_id]
1163
old_parent = tt.trans_id_tree_path(tree_path)
1164
_reparent_children(tt, old_parent, new_trans_id)
1168
divert_trans = set(file_trans_id[f] for f in divert)
1169
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1170
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1171
conflicts = cook_conflicts(raw_conflicts, tt)
1172
for conflict in conflicts:
1175
wt.add_conflicts(conflicts)
1176
except errors.UnsupportedOperation:
1184
def _reparent_children(tt, old_parent, new_parent):
1185
for child in tt.iter_tree_children(old_parent):
1186
tt.adjust_path(tt.final_name(child), new_parent, child)
1189
def _content_match(tree, entry, file_id, kind, target_path):
1190
if entry.kind != kind:
1192
if entry.kind == "directory":
1194
if entry.kind == "file":
1195
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1197
elif entry.kind == "symlink":
1198
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1203
def resolve_checkout(tt, conflicts, divert):
1204
new_conflicts = set()
1205
for c_type, conflict in ((c[0], c) for c in conflicts):
1206
# Anything but a 'duplicate' would indicate programmer error
1207
assert c_type == 'duplicate', c_type
1208
# Now figure out which is new and which is old
1209
if tt.new_contents(conflict[1]):
1210
new_file = conflict[1]
1211
old_file = conflict[2]
1213
new_file = conflict[2]
1214
old_file = conflict[1]
1216
# We should only get here if the conflict wasn't completely
1218
final_parent = tt.final_parent(old_file)
1219
if new_file in divert:
1220
new_name = tt.final_name(old_file)+'.diverted'
1221
tt.adjust_path(new_name, final_parent, new_file)
1222
new_conflicts.add((c_type, 'Diverted to',
1223
new_file, old_file))
1225
new_name = tt.final_name(old_file)+'.moved'
1226
tt.adjust_path(new_name, final_parent, old_file)
1227
new_conflicts.add((c_type, 'Moved existing file to',
1228
old_file, new_file))
1229
return new_conflicts
1232
def new_by_entry(tt, entry, parent_id, tree):
1233
"""Create a new file according to its inventory entry"""
1237
contents = tree.get_file(entry.file_id).readlines()
1238
executable = tree.is_executable(entry.file_id)
1239
return tt.new_file(name, parent_id, contents, entry.file_id,
1241
elif kind in ('directory', 'tree-reference'):
1242
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1243
if kind == 'tree-reference':
1244
tt.set_tree_reference(entry.reference_revision, trans_id)
1246
elif kind == 'symlink':
1247
target = tree.get_symlink_target(entry.file_id)
1248
return tt.new_symlink(name, parent_id, target, entry.file_id)
1250
raise errors.BadFileKindError(name, kind)
1252
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1253
"""Create new file contents according to an inventory entry."""
1254
if entry.kind == "file":
1256
lines = tree.get_file(entry.file_id).readlines()
1257
tt.create_file(lines, trans_id, mode_id=mode_id)
1258
elif entry.kind == "symlink":
1259
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1260
elif entry.kind == "directory":
1261
tt.create_directory(trans_id)
1263
def create_entry_executability(tt, entry, trans_id):
1264
"""Set the executability of a trans_id according to an inventory entry"""
1265
if entry.kind == "file":
1266
tt.set_executability(entry.executable, trans_id)
1269
def find_interesting(working_tree, target_tree, filenames):
1270
"""Find the ids corresponding to specified filenames."""
1271
trees = (working_tree, target_tree)
1272
return tree.find_ids_across_trees(filenames, trees)
1275
def change_entry(tt, file_id, working_tree, target_tree,
1276
trans_id_file_id, backups, trans_id, by_parent):
1277
"""Replace a file_id's contents with those from a target tree."""
1278
e_trans_id = trans_id_file_id(file_id)
1279
entry = target_tree.inventory[file_id]
1280
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1283
mode_id = e_trans_id
1286
tt.delete_contents(e_trans_id)
1288
parent_trans_id = trans_id_file_id(entry.parent_id)
1289
backup_name = get_backup_name(entry, by_parent,
1290
parent_trans_id, tt)
1291
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1292
tt.unversion_file(e_trans_id)
1293
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1294
tt.version_file(file_id, e_trans_id)
1295
trans_id[file_id] = e_trans_id
1296
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1297
create_entry_executability(tt, entry, e_trans_id)
1300
tt.set_executability(entry.executable, e_trans_id)
1301
if tt.final_name(e_trans_id) != entry.name:
1304
parent_id = tt.final_parent(e_trans_id)
1305
parent_file_id = tt.final_file_id(parent_id)
1306
if parent_file_id != entry.parent_id:
1311
parent_trans_id = trans_id_file_id(entry.parent_id)
1312
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1315
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1316
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1319
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1320
"""Produce a backup-style name that appears to be available"""
1324
yield "%s.~%d~" % (name, counter)
1326
for new_name in name_gen():
1327
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1331
def _entry_changes(file_id, entry, working_tree):
1332
"""Determine in which ways the inventory entry has changed.
1334
Returns booleans: has_contents, content_mod, meta_mod
1335
has_contents means there are currently contents, but they differ
1336
contents_mod means contents need to be modified
1337
meta_mod means the metadata needs to be modified
1339
cur_entry = working_tree.inventory[file_id]
1341
working_kind = working_tree.kind(file_id)
1344
has_contents = False
1347
if has_contents is True:
1348
if entry.kind != working_kind:
1349
contents_mod, meta_mod = True, False
1351
cur_entry._read_tree_state(working_tree.id2path(file_id),
1353
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1354
cur_entry._forget_tree_state()
1355
return has_contents, contents_mod, meta_mod
1358
def revert(working_tree, target_tree, filenames, backups=False,
1359
pb=DummyProgress(), change_reporter=None):
1360
"""Revert a working tree's contents to those of a target tree."""
1361
target_tree.lock_read()
1362
tt = TreeTransform(working_tree, pb)
1364
pp = ProgressPhase("Revert phase", 3, pb)
1366
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1368
_alter_files(working_tree, target_tree, tt, child_pb,
1373
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1375
raw_conflicts = resolve_conflicts(tt, child_pb)
1378
conflicts = cook_conflicts(raw_conflicts, tt)
1380
change_reporter = delta.ChangeReporter(working_tree.inventory)
1381
delta.report_changes(tt._iter_changes(), change_reporter)
1382
for conflict in conflicts:
1386
working_tree.set_merge_modified({})
1388
target_tree.unlock()
1394
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1396
merge_modified = working_tree.merge_modified()
1397
change_list = target_tree._iter_changes(working_tree,
1398
specific_files=specific_files, pb=pb)
1399
if target_tree.inventory.root is None:
1405
for id_num, (file_id, path, changed_content, versioned, parent, name,
1406
kind, executable) in enumerate(change_list):
1407
if skip_root and file_id[0] is not None and parent[0] is None:
1409
trans_id = tt.trans_id_file_id(file_id)
1412
keep_content = False
1413
if kind[0] == 'file' and (backups or kind[1] is None):
1414
wt_sha1 = working_tree.get_file_sha1(file_id)
1415
if merge_modified.get(file_id) != wt_sha1:
1416
# acquire the basis tree lazyily to prevent the expense
1417
# of accessing it when its not needed ? (Guessing, RBC,
1419
if basis_tree is None:
1420
basis_tree = working_tree.basis_tree()
1421
basis_tree.lock_read()
1422
if file_id in basis_tree:
1423
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1425
elif kind[1] is None and not versioned[1]:
1427
if kind[0] is not None:
1428
if not keep_content:
1429
tt.delete_contents(trans_id)
1430
elif kind[1] is not None:
1431
parent_trans_id = tt.trans_id_file_id(parent[0])
1432
by_parent = tt.by_parent()
1433
backup_name = _get_backup_name(name[0], by_parent,
1434
parent_trans_id, tt)
1435
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1436
new_trans_id = tt.create_path(name[0], parent_trans_id)
1437
if versioned == (True, True):
1438
tt.unversion_file(trans_id)
1439
tt.version_file(file_id, new_trans_id)
1440
# New contents should have the same unix perms as old
1443
trans_id = new_trans_id
1444
if kind[1] == 'directory':
1445
tt.create_directory(trans_id)
1446
elif kind[1] == 'symlink':
1447
tt.create_symlink(target_tree.get_symlink_target(file_id),
1449
elif kind[1] == 'file':
1450
tt.create_file(target_tree.get_file_lines(file_id),
1452
# preserve the execute bit when backing up
1453
if keep_content and executable[0] == executable[1]:
1454
tt.set_executability(executable[1], trans_id)
1456
assert kind[1] is None
1457
if versioned == (False, True):
1458
tt.version_file(file_id, trans_id)
1459
if versioned == (True, False):
1460
tt.unversion_file(trans_id)
1461
if (name[1] is not None and
1462
(name[0] != name[1] or parent[0] != parent[1])):
1464
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1465
if executable[0] != executable[1] and kind[1] == "file":
1466
tt.set_executability(executable[1], trans_id)
1468
if basis_tree is not None:
1472
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1473
"""Make many conflict-resolution attempts, but die if they fail"""
1474
if pass_func is None:
1475
pass_func = conflict_pass
1476
new_conflicts = set()
1479
pb.update('Resolution pass', n+1, 10)
1480
conflicts = tt.find_conflicts()
1481
if len(conflicts) == 0:
1482
return new_conflicts
1483
new_conflicts.update(pass_func(tt, conflicts))
1484
raise MalformedTransform(conflicts=conflicts)
1489
def conflict_pass(tt, conflicts):
1490
"""Resolve some classes of conflicts."""
1491
new_conflicts = set()
1492
for c_type, conflict in ((c[0], c) for c in conflicts):
1493
if c_type == 'duplicate id':
1494
tt.unversion_file(conflict[1])
1495
new_conflicts.add((c_type, 'Unversioned existing file',
1496
conflict[1], conflict[2], ))
1497
elif c_type == 'duplicate':
1498
# files that were renamed take precedence
1499
new_name = tt.final_name(conflict[1])+'.moved'
1500
final_parent = tt.final_parent(conflict[1])
1501
if tt.path_changed(conflict[1]):
1502
tt.adjust_path(new_name, final_parent, conflict[2])
1503
new_conflicts.add((c_type, 'Moved existing file to',
1504
conflict[2], conflict[1]))
1506
tt.adjust_path(new_name, final_parent, conflict[1])
1507
new_conflicts.add((c_type, 'Moved existing file to',
1508
conflict[1], conflict[2]))
1509
elif c_type == 'parent loop':
1510
# break the loop by undoing one of the ops that caused the loop
1512
while not tt.path_changed(cur):
1513
cur = tt.final_parent(cur)
1514
new_conflicts.add((c_type, 'Cancelled move', cur,
1515
tt.final_parent(cur),))
1516
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1518
elif c_type == 'missing parent':
1519
trans_id = conflict[1]
1521
tt.cancel_deletion(trans_id)
1522
new_conflicts.add(('deleting parent', 'Not deleting',
1525
tt.create_directory(trans_id)
1526
new_conflicts.add((c_type, 'Created directory', trans_id))
1527
elif c_type == 'unversioned parent':
1528
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1529
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1530
return new_conflicts
1533
def cook_conflicts(raw_conflicts, tt):
1534
"""Generate a list of cooked conflicts, sorted by file path"""
1535
from bzrlib.conflicts import Conflict
1536
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1537
return sorted(conflict_iter, key=Conflict.sort_key)
1540
def iter_cook_conflicts(raw_conflicts, tt):
1541
from bzrlib.conflicts import Conflict
1543
for conflict in raw_conflicts:
1544
c_type = conflict[0]
1545
action = conflict[1]
1546
modified_path = fp.get_path(conflict[2])
1547
modified_id = tt.final_file_id(conflict[2])
1548
if len(conflict) == 3:
1549
yield Conflict.factory(c_type, action=action, path=modified_path,
1550
file_id=modified_id)
1553
conflicting_path = fp.get_path(conflict[3])
1554
conflicting_id = tt.final_file_id(conflict[3])
1555
yield Conflict.factory(c_type, action=action, path=modified_path,
1556
file_id=modified_id,
1557
conflict_path=conflicting_path,
1558
conflict_file_id=conflicting_id)