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
for segment in contents:
280
self._set_mode(trans_id, mode_id, S_ISREG)
282
def _set_mode(self, trans_id, mode_id, typefunc):
283
"""Set the mode of new file contents.
284
The mode_id is the existing file to get the mode from (often the same
285
as trans_id). The operation is only performed if there's a mode match
286
according to typefunc.
291
old_path = self._tree_id_paths[mode_id]
295
mode = os.stat(self._tree.abspath(old_path)).st_mode
297
if e.errno == errno.ENOENT:
302
os.chmod(self._limbo_name(trans_id), mode)
304
def create_directory(self, trans_id):
305
"""Schedule creation of a new directory.
307
See also new_directory.
309
os.mkdir(self._limbo_name(trans_id))
310
unique_add(self._new_contents, trans_id, 'directory')
312
def create_symlink(self, target, trans_id):
313
"""Schedule creation of a new symbolic link.
315
target is a bytestring.
316
See also new_symlink.
318
os.symlink(target, self._limbo_name(trans_id))
319
unique_add(self._new_contents, trans_id, 'symlink')
321
def cancel_creation(self, trans_id):
322
"""Cancel the creation of new file contents."""
323
del self._new_contents[trans_id]
324
delete_any(self._limbo_name(trans_id))
326
def delete_contents(self, trans_id):
327
"""Schedule the contents of a path entry for deletion"""
328
self.tree_kind(trans_id)
329
self._removed_contents.add(trans_id)
331
def cancel_deletion(self, trans_id):
332
"""Cancel a scheduled deletion"""
333
self._removed_contents.remove(trans_id)
335
def unversion_file(self, trans_id):
336
"""Schedule a path entry to become unversioned"""
337
self._removed_id.add(trans_id)
339
def delete_versioned(self, trans_id):
340
"""Delete and unversion a versioned file"""
341
self.delete_contents(trans_id)
342
self.unversion_file(trans_id)
344
def set_executability(self, executability, trans_id):
345
"""Schedule setting of the 'execute' bit
346
To unschedule, set to None
348
if executability is None:
349
del self._new_executability[trans_id]
351
unique_add(self._new_executability, trans_id, executability)
353
def version_file(self, file_id, trans_id):
354
"""Schedule a file to become versioned."""
355
assert file_id is not None
356
unique_add(self._new_id, trans_id, file_id)
357
unique_add(self._r_new_id, file_id, trans_id)
359
def cancel_versioning(self, trans_id):
360
"""Undo a previous versioning of a file"""
361
file_id = self._new_id[trans_id]
362
del self._new_id[trans_id]
363
del self._r_new_id[file_id]
366
"""Determine the paths of all new and changed files"""
368
fp = FinalPaths(self)
369
for id_set in (self._new_name, self._new_parent, self._new_contents,
370
self._new_id, self._new_executability):
371
new_ids.update(id_set)
372
new_paths = [(fp.get_path(t), t) for t in new_ids]
376
def tree_kind(self, trans_id):
377
"""Determine the file kind in the working tree.
379
Raises NoSuchFile if the file does not exist
381
path = self._tree_id_paths.get(trans_id)
383
raise NoSuchFile(None)
385
return file_kind(self._tree.abspath(path))
387
if e.errno != errno.ENOENT:
390
raise NoSuchFile(path)
392
def final_kind(self, trans_id):
393
"""Determine the final file kind, after any changes applied.
395
Raises NoSuchFile if the file does not exist/has no contents.
396
(It is conceivable that a path would be created without the
397
corresponding contents insertion command)
399
if trans_id in self._new_contents:
400
return self._new_contents[trans_id]
401
elif trans_id in self._removed_contents:
402
raise NoSuchFile(None)
404
return self.tree_kind(trans_id)
406
def tree_file_id(self, trans_id):
407
"""Determine the file id associated with the trans_id in the tree"""
409
path = self._tree_id_paths[trans_id]
411
# the file is a new, unversioned file, or invalid trans_id
413
# the file is old; the old id is still valid
414
if self._new_root == trans_id:
415
return self._tree.inventory.root.file_id
416
return self._tree.inventory.path2id(path)
418
def final_file_id(self, trans_id):
419
"""Determine the file id after any changes are applied, or None.
421
None indicates that the file will not be versioned after changes are
425
# there is a new id for this file
426
assert self._new_id[trans_id] is not None
427
return self._new_id[trans_id]
429
if trans_id in self._removed_id:
431
return self.tree_file_id(trans_id)
433
def inactive_file_id(self, trans_id):
434
"""Return the inactive file_id associated with a transaction id.
435
That is, the one in the tree or in non_present_ids.
436
The file_id may actually be active, too.
438
file_id = self.tree_file_id(trans_id)
439
if file_id is not None:
441
for key, value in self._non_present_ids.iteritems():
442
if value == trans_id:
445
def final_parent(self, trans_id):
446
"""Determine the parent file_id, after any changes are applied.
448
ROOT_PARENT is returned for the tree root.
451
return self._new_parent[trans_id]
453
return self.get_tree_parent(trans_id)
455
def final_name(self, trans_id):
456
"""Determine the final filename, after all changes are applied."""
458
return self._new_name[trans_id]
461
return os.path.basename(self._tree_id_paths[trans_id])
463
raise NoFinalPath(trans_id, self)
466
"""Return a map of parent: children for known parents.
468
Only new paths and parents of tree files with assigned ids are used.
471
items = list(self._new_parent.iteritems())
472
items.extend((t, self.final_parent(t)) for t in
473
self._tree_id_paths.keys())
474
for trans_id, parent_id in items:
475
if parent_id not in by_parent:
476
by_parent[parent_id] = set()
477
by_parent[parent_id].add(trans_id)
480
def path_changed(self, trans_id):
481
"""Return True if a trans_id's path has changed."""
482
return (trans_id in self._new_name) or (trans_id in self._new_parent)
484
def new_contents(self, trans_id):
485
return (trans_id in self._new_contents)
487
def find_conflicts(self):
488
"""Find any violations of inventory or filesystem invariants"""
489
if self.__done is True:
490
raise ReusingTransform()
492
# ensure all children of all existent parents are known
493
# all children of non-existent parents are known, by definition.
494
self._add_tree_children()
495
by_parent = self.by_parent()
496
conflicts.extend(self._unversioned_parents(by_parent))
497
conflicts.extend(self._parent_loops())
498
conflicts.extend(self._duplicate_entries(by_parent))
499
conflicts.extend(self._duplicate_ids())
500
conflicts.extend(self._parent_type_conflicts(by_parent))
501
conflicts.extend(self._improper_versioning())
502
conflicts.extend(self._executability_conflicts())
503
conflicts.extend(self._overwrite_conflicts())
506
def _add_tree_children(self):
507
"""Add all the children of all active parents to the known paths.
509
Active parents are those which gain children, and those which are
510
removed. This is a necessary first step in detecting conflicts.
512
parents = self.by_parent().keys()
513
parents.extend([t for t in self._removed_contents if
514
self.tree_kind(t) == 'directory'])
515
for trans_id in self._removed_id:
516
file_id = self.tree_file_id(trans_id)
517
if self._tree.inventory[file_id].kind == 'directory':
518
parents.append(trans_id)
520
for parent_id in parents:
521
# ensure that all children are registered with the transaction
522
list(self.iter_tree_children(parent_id))
524
def iter_tree_children(self, parent_id):
525
"""Iterate through the entry's tree children, if any"""
527
path = self._tree_id_paths[parent_id]
531
children = os.listdir(self._tree.abspath(path))
533
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
537
for child in children:
538
childpath = joinpath(path, child)
539
if self._tree.is_control_filename(childpath):
541
yield self.trans_id_tree_path(childpath)
543
def has_named_child(self, by_parent, parent_id, name):
545
children = by_parent[parent_id]
548
for child in children:
549
if self.final_name(child) == name:
552
path = self._tree_id_paths[parent_id]
555
childpath = joinpath(path, name)
556
child_id = self._tree_path_ids.get(childpath)
558
return lexists(self._tree.abspath(childpath))
560
if self.final_parent(child_id) != parent_id:
562
if child_id in self._removed_contents:
563
# XXX What about dangling file-ids?
568
def _parent_loops(self):
569
"""No entry should be its own ancestor"""
571
for trans_id in self._new_parent:
574
while parent_id is not ROOT_PARENT:
577
parent_id = self.final_parent(parent_id)
580
if parent_id == trans_id:
581
conflicts.append(('parent loop', trans_id))
582
if parent_id in seen:
586
def _unversioned_parents(self, by_parent):
587
"""If parent directories are versioned, children must be versioned."""
589
for parent_id, children in by_parent.iteritems():
590
if parent_id is ROOT_PARENT:
592
if self.final_file_id(parent_id) is not None:
594
for child_id in children:
595
if self.final_file_id(child_id) is not None:
596
conflicts.append(('unversioned parent', parent_id))
600
def _improper_versioning(self):
601
"""Cannot version a file with no contents, or a bad type.
603
However, existing entries with no contents are okay.
606
for trans_id in self._new_id.iterkeys():
608
kind = self.final_kind(trans_id)
610
conflicts.append(('versioning no contents', trans_id))
612
if not InventoryEntry.versionable_kind(kind):
613
conflicts.append(('versioning bad kind', trans_id, kind))
616
def _executability_conflicts(self):
617
"""Check for bad executability changes.
619
Only versioned files may have their executability set, because
620
1. only versioned entries can have executability under windows
621
2. only files can be executable. (The execute bit on a directory
622
does not indicate searchability)
625
for trans_id in self._new_executability:
626
if self.final_file_id(trans_id) is None:
627
conflicts.append(('unversioned executability', trans_id))
630
non_file = self.final_kind(trans_id) != "file"
634
conflicts.append(('non-file executability', trans_id))
637
def _overwrite_conflicts(self):
638
"""Check for overwrites (not permitted on Win32)"""
640
for trans_id in self._new_contents:
642
self.tree_kind(trans_id)
645
if trans_id not in self._removed_contents:
646
conflicts.append(('overwrite', trans_id,
647
self.final_name(trans_id)))
650
def _duplicate_entries(self, by_parent):
651
"""No directory may have two entries with the same name."""
653
for children in by_parent.itervalues():
654
name_ids = [(self.final_name(t), t) for t in children]
658
for name, trans_id in name_ids:
660
kind = self.final_kind(trans_id)
663
file_id = self.final_file_id(trans_id)
664
if kind is None and file_id is None:
666
if name == last_name:
667
conflicts.append(('duplicate', last_trans_id, trans_id,
670
last_trans_id = trans_id
673
def _duplicate_ids(self):
674
"""Each inventory id may only be used once"""
676
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
678
active_tree_ids = set((f for f in self._tree.inventory if
679
f not in removed_tree_ids))
680
for trans_id, file_id in self._new_id.iteritems():
681
if file_id in active_tree_ids:
682
old_trans_id = self.trans_id_tree_file_id(file_id)
683
conflicts.append(('duplicate id', old_trans_id, trans_id))
686
def _parent_type_conflicts(self, by_parent):
687
"""parents must have directory 'contents'."""
689
for parent_id, children in by_parent.iteritems():
690
if parent_id is ROOT_PARENT:
692
if not self._any_contents(children):
694
for child in children:
696
self.final_kind(child)
700
kind = self.final_kind(parent_id)
704
conflicts.append(('missing parent', parent_id))
705
elif kind != "directory":
706
conflicts.append(('non-directory parent', parent_id))
709
def _any_contents(self, trans_ids):
710
"""Return true if any of the trans_ids, will have contents."""
711
for trans_id in trans_ids:
713
kind = self.final_kind(trans_id)
720
"""Apply all changes to the inventory and filesystem.
722
If filesystem or inventory conflicts are present, MalformedTransform
725
conflicts = self.find_conflicts()
726
if len(conflicts) != 0:
727
raise MalformedTransform(conflicts=conflicts)
729
inv = self._tree.inventory
730
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
732
child_pb.update('Apply phase', 0, 2)
733
self._apply_removals(inv, limbo_inv)
734
child_pb.update('Apply phase', 1, 2)
735
modified_paths = self._apply_insertions(inv, limbo_inv)
738
self._tree._write_inventory(inv)
741
return _TransformResults(modified_paths)
743
def _limbo_name(self, trans_id):
744
"""Generate the limbo name of a file"""
745
return pathjoin(self._limbodir, trans_id)
747
def _apply_removals(self, inv, limbo_inv):
748
"""Perform tree operations that remove directory/inventory names.
750
That is, delete files that are to be deleted, and put any files that
751
need renaming into limbo. This must be done in strict child-to-parent
754
tree_paths = list(self._tree_path_ids.iteritems())
755
tree_paths.sort(reverse=True)
756
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
758
for num, data in enumerate(tree_paths):
759
path, trans_id = data
760
child_pb.update('removing file', num, len(tree_paths))
761
full_path = self._tree.abspath(path)
762
if trans_id in self._removed_contents:
763
delete_any(full_path)
764
elif trans_id in self._new_name or trans_id in \
767
os.rename(full_path, self._limbo_name(trans_id))
769
if e.errno != errno.ENOENT:
771
if trans_id in self._removed_id:
772
if trans_id == self._new_root:
773
file_id = self._tree.inventory.root.file_id
775
file_id = self.tree_file_id(trans_id)
777
elif trans_id in self._new_name or trans_id in self._new_parent:
778
file_id = self.tree_file_id(trans_id)
779
if file_id is not None:
780
limbo_inv[trans_id] = inv[file_id]
785
def _apply_insertions(self, inv, limbo_inv):
786
"""Perform tree operations that insert directory/inventory names.
788
That is, create any files that need to be created, and restore from
789
limbo any files that needed renaming. This must be done in strict
790
parent-to-child order.
792
new_paths = self.new_paths()
794
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
796
for num, (path, trans_id) in enumerate(new_paths):
797
child_pb.update('adding file', num, len(new_paths))
799
kind = self._new_contents[trans_id]
801
kind = contents = None
802
if trans_id in self._new_contents or \
803
self.path_changed(trans_id):
804
full_path = self._tree.abspath(path)
806
os.rename(self._limbo_name(trans_id), full_path)
808
# We may be renaming a dangling inventory id
809
if e.errno != errno.ENOENT:
811
if trans_id in self._new_contents:
812
modified_paths.append(full_path)
813
del self._new_contents[trans_id]
815
if trans_id in self._new_id:
817
kind = file_kind(self._tree.abspath(path))
818
inv.add_path(path, kind, self._new_id[trans_id])
819
elif trans_id in self._new_name or trans_id in\
821
entry = limbo_inv.get(trans_id)
822
if entry is not None:
823
entry.name = self.final_name(trans_id)
824
parent_path = os.path.dirname(path)
826
self._tree.inventory.path2id(parent_path)
829
# requires files and inventory entries to be in place
830
if trans_id in self._new_executability:
831
self._set_executability(path, inv, trans_id)
834
return modified_paths
836
def _set_executability(self, path, inv, trans_id):
837
"""Set the executability of versioned files """
838
file_id = inv.path2id(path)
839
new_executability = self._new_executability[trans_id]
840
inv[file_id].executable = new_executability
841
if supports_executable():
842
abspath = self._tree.abspath(path)
843
current_mode = os.stat(abspath).st_mode
844
if new_executability:
847
to_mode = current_mode | (0100 & ~umask)
848
# Enable x-bit for others only if they can read it.
849
if current_mode & 0004:
850
to_mode |= 0001 & ~umask
851
if current_mode & 0040:
852
to_mode |= 0010 & ~umask
854
to_mode = current_mode & ~0111
855
os.chmod(abspath, to_mode)
857
def _new_entry(self, name, parent_id, file_id):
858
"""Helper function to create a new filesystem entry."""
859
trans_id = self.create_path(name, parent_id)
860
if file_id is not None:
861
self.version_file(file_id, trans_id)
864
def new_file(self, name, parent_id, contents, file_id=None,
866
"""Convenience method to create files.
868
name is the name of the file to create.
869
parent_id is the transaction id of the parent directory of the file.
870
contents is an iterator of bytestrings, which will be used to produce
872
:param file_id: The inventory ID of the file, if it is to be versioned.
873
:param executable: Only valid when a file_id has been supplied.
875
trans_id = self._new_entry(name, parent_id, file_id)
876
# TODO: rather than scheduling a set_executable call,
877
# have create_file create the file with the right mode.
878
self.create_file(contents, trans_id)
879
if executable is not None:
880
self.set_executability(executable, trans_id)
883
def new_directory(self, name, parent_id, file_id=None):
884
"""Convenience method to create directories.
886
name is the name of the directory to create.
887
parent_id is the transaction id of the parent directory of the
889
file_id is the inventory ID of the directory, if it is to be versioned.
891
trans_id = self._new_entry(name, parent_id, file_id)
892
self.create_directory(trans_id)
895
def new_symlink(self, name, parent_id, target, file_id=None):
896
"""Convenience method to create symbolic link.
898
name is the name of the symlink to create.
899
parent_id is the transaction id of the parent directory of the symlink.
900
target is a bytestring of the target of the symlink.
901
file_id is the inventory ID of the file, if it is to be versioned.
903
trans_id = self._new_entry(name, parent_id, file_id)
904
self.create_symlink(target, trans_id)
907
def joinpath(parent, child):
908
"""Join tree-relative paths, handling the tree root specially"""
909
if parent is None or parent == "":
912
return pathjoin(parent, child)
915
class FinalPaths(object):
916
"""Make path calculation cheap by memoizing paths.
918
The underlying tree must not be manipulated between calls, or else
919
the results will likely be incorrect.
921
def __init__(self, transform):
922
object.__init__(self)
923
self._known_paths = {}
924
self.transform = transform
926
def _determine_path(self, trans_id):
927
if trans_id == self.transform.root:
929
name = self.transform.final_name(trans_id)
930
parent_id = self.transform.final_parent(trans_id)
931
if parent_id == self.transform.root:
934
return pathjoin(self.get_path(parent_id), name)
936
def get_path(self, trans_id):
937
"""Find the final path associated with a trans_id"""
938
if trans_id not in self._known_paths:
939
self._known_paths[trans_id] = self._determine_path(trans_id)
940
return self._known_paths[trans_id]
942
def topology_sorted_ids(tree):
943
"""Determine the topological order of the ids in a tree"""
944
file_ids = list(tree)
945
file_ids.sort(key=tree.id2path)
949
def build_tree(tree, wt):
950
"""Create working tree for a branch, using a TreeTransform.
952
This function should be used on empty trees, having a tree root at most.
953
(see merge and revert functionality for working with existing trees)
955
Existing files are handled like so:
957
- Existing bzrdirs take precedence over creating new items. They are
958
created as '%s.diverted' % name.
959
- Otherwise, if the content on disk matches the content we are building,
960
it is silently replaced.
961
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
963
if len(wt.inventory) > 1: # more than just a root
964
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
966
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
967
pp = ProgressPhase("Build phase", 2, top_pb)
968
if tree.inventory.root is not None:
969
wt.set_root_id(tree.inventory.root.file_id)
970
tt = TreeTransform(wt)
974
file_trans_id[wt.get_root_id()] = \
975
tt.trans_id_tree_file_id(wt.get_root_id())
976
pb = bzrlib.ui.ui_factory.nested_progress_bar()
978
for num, (tree_path, entry) in \
979
enumerate(tree.inventory.iter_entries_by_dir()):
980
pb.update("Building tree", num, len(tree.inventory))
981
if entry.parent_id is None:
984
file_id = entry.file_id
985
target_path = wt.abspath(tree_path)
987
kind = file_kind(target_path)
991
if kind == "directory":
993
bzrdir.BzrDir.open(target_path)
994
except errors.NotBranchError:
998
if (file_id not in divert and
999
_content_match(tree, entry, file_id, kind,
1001
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1002
if kind == 'directory':
1004
if entry.parent_id not in file_trans_id:
1005
raise repr(entry.parent_id)
1006
parent_id = file_trans_id[entry.parent_id]
1007
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1010
new_trans_id = file_trans_id[file_id]
1011
old_parent = tt.trans_id_tree_path(tree_path)
1012
_reparent_children(tt, old_parent, new_trans_id)
1016
divert_trans = set(file_trans_id[f] for f in divert)
1017
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1018
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1019
conflicts = cook_conflicts(raw_conflicts, tt)
1020
for conflict in conflicts:
1023
wt.add_conflicts(conflicts)
1024
except errors.UnsupportedOperation:
1032
def _reparent_children(tt, old_parent, new_parent):
1033
for child in tt.iter_tree_children(old_parent):
1034
tt.adjust_path(tt.final_name(child), new_parent, child)
1037
def _content_match(tree, entry, file_id, kind, target_path):
1038
if entry.kind != kind:
1040
if entry.kind == "directory":
1042
if entry.kind == "file":
1043
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1045
elif entry.kind == "symlink":
1046
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1051
def resolve_checkout(tt, conflicts, divert):
1052
new_conflicts = set()
1053
for c_type, conflict in ((c[0], c) for c in conflicts):
1054
# Anything but a 'duplicate' would indicate programmer error
1055
assert c_type == 'duplicate', c_type
1056
# Now figure out which is new and which is old
1057
if tt.new_contents(conflict[1]):
1058
new_file = conflict[1]
1059
old_file = conflict[2]
1061
new_file = conflict[2]
1062
old_file = conflict[1]
1064
# We should only get here if the conflict wasn't completely
1066
final_parent = tt.final_parent(old_file)
1067
if new_file in divert:
1068
new_name = tt.final_name(old_file)+'.diverted'
1069
tt.adjust_path(new_name, final_parent, new_file)
1070
new_conflicts.add((c_type, 'Diverted to',
1071
new_file, old_file))
1073
new_name = tt.final_name(old_file)+'.moved'
1074
tt.adjust_path(new_name, final_parent, old_file)
1075
new_conflicts.add((c_type, 'Moved existing file to',
1076
old_file, new_file))
1077
return new_conflicts
1080
def new_by_entry(tt, entry, parent_id, tree):
1081
"""Create a new file according to its inventory entry"""
1085
contents = tree.get_file(entry.file_id).readlines()
1086
executable = tree.is_executable(entry.file_id)
1087
return tt.new_file(name, parent_id, contents, entry.file_id,
1089
elif kind == 'directory':
1090
return tt.new_directory(name, parent_id, entry.file_id)
1091
elif kind == 'symlink':
1092
target = tree.get_symlink_target(entry.file_id)
1093
return tt.new_symlink(name, parent_id, target, entry.file_id)
1095
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1096
"""Create new file contents according to an inventory entry."""
1097
if entry.kind == "file":
1099
lines = tree.get_file(entry.file_id).readlines()
1100
tt.create_file(lines, trans_id, mode_id=mode_id)
1101
elif entry.kind == "symlink":
1102
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1103
elif entry.kind == "directory":
1104
tt.create_directory(trans_id)
1106
def create_entry_executability(tt, entry, trans_id):
1107
"""Set the executability of a trans_id according to an inventory entry"""
1108
if entry.kind == "file":
1109
tt.set_executability(entry.executable, trans_id)
1112
def find_interesting(working_tree, target_tree, filenames):
1113
"""Find the ids corresponding to specified filenames."""
1114
trees = (working_tree, target_tree)
1115
return tree.find_ids_across_trees(filenames, trees)
1118
def change_entry(tt, file_id, working_tree, target_tree,
1119
trans_id_file_id, backups, trans_id, by_parent):
1120
"""Replace a file_id's contents with those from a target tree."""
1121
e_trans_id = trans_id_file_id(file_id)
1122
entry = target_tree.inventory[file_id]
1123
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1126
mode_id = e_trans_id
1129
tt.delete_contents(e_trans_id)
1131
parent_trans_id = trans_id_file_id(entry.parent_id)
1132
backup_name = get_backup_name(entry, by_parent,
1133
parent_trans_id, tt)
1134
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1135
tt.unversion_file(e_trans_id)
1136
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1137
tt.version_file(file_id, e_trans_id)
1138
trans_id[file_id] = e_trans_id
1139
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1140
create_entry_executability(tt, entry, e_trans_id)
1143
tt.set_executability(entry.executable, e_trans_id)
1144
if tt.final_name(e_trans_id) != entry.name:
1147
parent_id = tt.final_parent(e_trans_id)
1148
parent_file_id = tt.final_file_id(parent_id)
1149
if parent_file_id != entry.parent_id:
1154
parent_trans_id = trans_id_file_id(entry.parent_id)
1155
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1158
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1159
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1162
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1163
"""Produce a backup-style name that appears to be available"""
1167
yield "%s.~%d~" % (name, counter)
1169
for new_name in name_gen():
1170
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1174
def _entry_changes(file_id, entry, working_tree):
1175
"""Determine in which ways the inventory entry has changed.
1177
Returns booleans: has_contents, content_mod, meta_mod
1178
has_contents means there are currently contents, but they differ
1179
contents_mod means contents need to be modified
1180
meta_mod means the metadata needs to be modified
1182
cur_entry = working_tree.inventory[file_id]
1184
working_kind = working_tree.kind(file_id)
1187
has_contents = False
1190
if has_contents is True:
1191
if entry.kind != working_kind:
1192
contents_mod, meta_mod = True, False
1194
cur_entry._read_tree_state(working_tree.id2path(file_id),
1196
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1197
cur_entry._forget_tree_state()
1198
return has_contents, contents_mod, meta_mod
1201
def revert(working_tree, target_tree, filenames, backups=False,
1202
pb=DummyProgress()):
1203
"""Revert a working tree's contents to those of a target tree."""
1204
interesting_ids = find_interesting(working_tree, target_tree, filenames)
1205
tt = TreeTransform(working_tree, pb)
1207
pp = ProgressPhase("Revert phase", 3, pb)
1209
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1211
_alter_files(working_tree, target_tree, tt, child_pb,
1212
interesting_ids, backups)
1216
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1218
raw_conflicts = resolve_conflicts(tt, child_pb)
1221
conflicts = cook_conflicts(raw_conflicts, tt)
1222
for conflict in conflicts:
1226
working_tree.set_merge_modified({})
1233
def _alter_files(working_tree, target_tree, tt, pb, interesting_ids, backups):
1234
merge_modified = working_tree.merge_modified()
1235
iterator = target_tree._iter_changes(working_tree,
1236
specific_file_ids=interesting_ids,
1238
if target_tree.inventory.root is None:
1243
for id_num, (file_id, path, changed_content, versioned, parent, name, kind,
1244
executable) in enumerate(iterator):
1245
if skip_root and file_id[0] is not None and parent[0] is None:
1247
trans_id = tt.trans_id_file_id(file_id)
1250
keep_content = False
1251
if kind[0] == 'file' and (backups or kind[1] is None):
1252
wt_sha1 = working_tree.get_file_sha1(file_id)
1253
if merge_modified.get(file_id) != wt_sha1:
1254
if basis_tree is None:
1255
basis_tree = working_tree.basis_tree()
1256
if file_id in basis_tree:
1257
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1259
elif kind[1] is None and not versioned[1]:
1261
if kind[0] is not None:
1262
if not keep_content:
1263
tt.delete_contents(trans_id)
1264
elif kind[1] is not None:
1265
parent_trans_id = tt.trans_id_file_id(parent[0])
1266
by_parent = tt.by_parent()
1267
backup_name = _get_backup_name(name[0], by_parent,
1268
parent_trans_id, tt)
1269
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1270
new_trans_id = tt.create_path(name[0], parent_trans_id)
1271
if versioned == (True, True):
1272
tt.unversion_file(trans_id)
1273
tt.version_file(file_id, new_trans_id)
1274
# New contents should have the same unix perms as old
1277
trans_id = new_trans_id
1278
if kind[1] == 'directory':
1279
tt.create_directory(trans_id)
1280
elif kind[1] == 'symlink':
1281
tt.create_symlink(target_tree.get_symlink_target(file_id),
1283
elif kind[1] == 'file':
1284
tt.create_file(target_tree.get_file_lines(file_id),
1286
# preserve the execute bit when backing up
1287
if keep_content and executable[0] == executable[1]:
1288
tt.set_executability(executable[1], trans_id)
1290
assert kind[1] is None
1291
if versioned == (False, True):
1292
tt.version_file(file_id, trans_id)
1293
if versioned == (True, False):
1294
tt.unversion_file(trans_id)
1295
if (name[1] is not None and
1296
(name[0] != name[1] or parent[0] != parent[1])):
1297
tt.adjust_path(name[1], tt.trans_id_file_id(parent[1]), trans_id)
1298
if executable[0] != executable[1] and kind[1] == "file":
1299
tt.set_executability(executable[1], trans_id)
1302
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1303
"""Make many conflict-resolution attempts, but die if they fail"""
1304
if pass_func is None:
1305
pass_func = conflict_pass
1306
new_conflicts = set()
1309
pb.update('Resolution pass', n+1, 10)
1310
conflicts = tt.find_conflicts()
1311
if len(conflicts) == 0:
1312
return new_conflicts
1313
new_conflicts.update(pass_func(tt, conflicts))
1314
raise MalformedTransform(conflicts=conflicts)
1319
def conflict_pass(tt, conflicts):
1320
"""Resolve some classes of conflicts."""
1321
new_conflicts = set()
1322
for c_type, conflict in ((c[0], c) for c in conflicts):
1323
if c_type == 'duplicate id':
1324
tt.unversion_file(conflict[1])
1325
new_conflicts.add((c_type, 'Unversioned existing file',
1326
conflict[1], conflict[2], ))
1327
elif c_type == 'duplicate':
1328
# files that were renamed take precedence
1329
new_name = tt.final_name(conflict[1])+'.moved'
1330
final_parent = tt.final_parent(conflict[1])
1331
if tt.path_changed(conflict[1]):
1332
tt.adjust_path(new_name, final_parent, conflict[2])
1333
new_conflicts.add((c_type, 'Moved existing file to',
1334
conflict[2], conflict[1]))
1336
tt.adjust_path(new_name, final_parent, conflict[1])
1337
new_conflicts.add((c_type, 'Moved existing file to',
1338
conflict[1], conflict[2]))
1339
elif c_type == 'parent loop':
1340
# break the loop by undoing one of the ops that caused the loop
1342
while not tt.path_changed(cur):
1343
cur = tt.final_parent(cur)
1344
new_conflicts.add((c_type, 'Cancelled move', cur,
1345
tt.final_parent(cur),))
1346
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1348
elif c_type == 'missing parent':
1349
trans_id = conflict[1]
1351
tt.cancel_deletion(trans_id)
1352
new_conflicts.add(('deleting parent', 'Not deleting',
1355
tt.create_directory(trans_id)
1356
new_conflicts.add((c_type, 'Created directory', trans_id))
1357
elif c_type == 'unversioned parent':
1358
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1359
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1360
return new_conflicts
1363
def cook_conflicts(raw_conflicts, tt):
1364
"""Generate a list of cooked conflicts, sorted by file path"""
1365
from bzrlib.conflicts import Conflict
1366
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1367
return sorted(conflict_iter, key=Conflict.sort_key)
1370
def iter_cook_conflicts(raw_conflicts, tt):
1371
from bzrlib.conflicts import Conflict
1373
for conflict in raw_conflicts:
1374
c_type = conflict[0]
1375
action = conflict[1]
1376
modified_path = fp.get_path(conflict[2])
1377
modified_id = tt.final_file_id(conflict[2])
1378
if len(conflict) == 3:
1379
yield Conflict.factory(c_type, action=action, path=modified_path,
1380
file_id=modified_id)
1383
conflicting_path = fp.get_path(conflict[3])
1384
conflicting_id = tt.final_file_id(conflict[3])
1385
yield Conflict.factory(c_type, action=action, path=modified_path,
1386
file_id=modified_id,
1387
conflict_path=conflicting_path,
1388
conflict_file_id=conflicting_id)