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.lazy_import import lazy_import
23
lazy_import(globals(), """
24
from bzrlib import delta
26
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
27
ReusingTransform, NotVersionedError, CantMoveRoot,
28
ExistingLimbo, ImmortalLimbo, NoFinalPath)
29
from bzrlib.inventory import InventoryEntry
30
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
32
from bzrlib.progress import DummyProgress, ProgressPhase
33
from bzrlib.trace import mutter, warning
34
from bzrlib import tree
36
import bzrlib.urlutils as urlutils
39
ROOT_PARENT = "root-parent"
42
def unique_add(map, key, value):
44
raise DuplicateKey(key=key)
48
class _TransformResults(object):
49
def __init__(self, modified_paths):
51
self.modified_paths = modified_paths
54
class TreeTransform(object):
55
"""Represent a tree transformation.
57
This object is designed to support incremental generation of the transform,
60
It is easy to produce malformed transforms, but they are generally
61
harmless. Attempting to apply a malformed transform will cause an
62
exception to be raised before any modifications are made to the tree.
64
Many kinds of malformed transforms can be corrected with the
65
resolve_conflicts function. The remaining ones indicate programming error,
66
such as trying to create a file with no path.
68
Two sets of file creation methods are supplied. Convenience methods are:
73
These are composed of the low-level methods:
75
* create_file or create_directory or create_symlink
79
def __init__(self, tree, pb=DummyProgress()):
80
"""Note: a tree_write lock is taken on the tree.
82
Use TreeTransform.finalize() to release the lock
86
self._tree.lock_tree_write()
88
control_files = self._tree._control_files
89
self._limbodir = urlutils.local_path_from_url(
90
control_files.controlfilename('limbo'))
92
os.mkdir(self._limbodir)
94
if e.errno == errno.EEXIST:
95
raise ExistingLimbo(self._limbodir)
102
self._new_parent = {}
103
self._new_contents = {}
104
self._removed_contents = set()
105
self._new_executability = {}
107
self._non_present_ids = {}
109
self._removed_id = set()
110
self._tree_path_ids = {}
111
self._tree_id_paths = {}
113
# Cache of realpath results, to speed up canonical_path
115
# Cache of relpath results, to speed up canonical_path
116
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
120
def __get_root(self):
121
return self._new_root
123
root = property(__get_root)
126
"""Release the working tree lock, if held, clean up limbo dir."""
127
if self._tree is None:
130
for trans_id, kind in self._new_contents.iteritems():
131
path = self._limbo_name(trans_id)
132
if kind == "directory":
137
os.rmdir(self._limbodir)
139
# We don't especially care *why* the dir is immortal.
140
raise ImmortalLimbo(self._limbodir)
145
def _assign_id(self):
146
"""Produce a new tranform id"""
147
new_id = "new-%s" % self._id_number
151
def create_path(self, name, parent):
152
"""Assign a transaction id to a new path"""
153
trans_id = self._assign_id()
154
unique_add(self._new_name, trans_id, name)
155
unique_add(self._new_parent, trans_id, parent)
158
def adjust_path(self, name, parent, trans_id):
159
"""Change the path that is assigned to a transaction id."""
160
if trans_id == self._new_root:
162
self._new_name[trans_id] = name
163
self._new_parent[trans_id] = parent
165
def adjust_root_path(self, name, parent):
166
"""Emulate moving the root by moving all children, instead.
168
We do this by undoing the association of root's transaction id with the
169
current tree. This allows us to create a new directory with that
170
transaction id. We unversion the root directory and version the
171
physically new directory, and hope someone versions the tree root
174
old_root = self._new_root
175
old_root_file_id = self.final_file_id(old_root)
176
# force moving all children of root
177
for child_id in self.iter_tree_children(old_root):
178
if child_id != parent:
179
self.adjust_path(self.final_name(child_id),
180
self.final_parent(child_id), child_id)
181
file_id = self.final_file_id(child_id)
182
if file_id is not None:
183
self.unversion_file(child_id)
184
self.version_file(file_id, child_id)
186
# the physical root needs a new transaction id
187
self._tree_path_ids.pop("")
188
self._tree_id_paths.pop(old_root)
189
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
190
if parent == old_root:
191
parent = self._new_root
192
self.adjust_path(name, parent, old_root)
193
self.create_directory(old_root)
194
self.version_file(old_root_file_id, old_root)
195
self.unversion_file(self._new_root)
197
def trans_id_tree_file_id(self, inventory_id):
198
"""Determine the transaction id of a working tree file.
200
This reflects only files that already exist, not ones that will be
201
added by transactions.
203
path = self._tree.inventory.id2path(inventory_id)
204
return self.trans_id_tree_path(path)
206
def trans_id_file_id(self, file_id):
207
"""Determine or set the transaction id associated with a file ID.
208
A new id is only created for file_ids that were never present. If
209
a transaction has been unversioned, it is deliberately still returned.
210
(this will likely lead to an unversioned parent conflict.)
212
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
213
return self._r_new_id[file_id]
214
elif file_id in self._tree.inventory:
215
return self.trans_id_tree_file_id(file_id)
216
elif file_id in self._non_present_ids:
217
return self._non_present_ids[file_id]
219
trans_id = self._assign_id()
220
self._non_present_ids[file_id] = trans_id
223
def canonical_path(self, path):
224
"""Get the canonical tree-relative path"""
225
# don't follow final symlinks
226
abs = self._tree.abspath(path)
227
if abs in self._relpaths:
228
return self._relpaths[abs]
229
dirname, basename = os.path.split(abs)
230
if dirname not in self._realpaths:
231
self._realpaths[dirname] = os.path.realpath(dirname)
232
dirname = self._realpaths[dirname]
233
abs = pathjoin(dirname, basename)
234
if dirname in self._relpaths:
235
relpath = pathjoin(self._relpaths[dirname], basename)
236
relpath = relpath.rstrip('/\\')
238
relpath = self._tree.relpath(abs)
239
self._relpaths[abs] = relpath
242
def trans_id_tree_path(self, path):
243
"""Determine (and maybe set) the transaction ID for a tree path."""
244
path = self.canonical_path(path)
245
if path not in self._tree_path_ids:
246
self._tree_path_ids[path] = self._assign_id()
247
self._tree_id_paths[self._tree_path_ids[path]] = path
248
return self._tree_path_ids[path]
250
def get_tree_parent(self, trans_id):
251
"""Determine id of the parent in the tree."""
252
path = self._tree_id_paths[trans_id]
255
return self.trans_id_tree_path(os.path.dirname(path))
257
def create_file(self, contents, trans_id, mode_id=None):
258
"""Schedule creation of a new file.
262
Contents is an iterator of strings, all of which will be written
263
to the target destination.
265
New file takes the permissions of any existing file with that id,
266
unless mode_id is specified.
268
name = self._limbo_name(trans_id)
272
unique_add(self._new_contents, trans_id, 'file')
274
# Clean up the file, it never got registered so
275
# TreeTransform.finalize() won't clean it up.
280
f.writelines(contents)
283
self._set_mode(trans_id, mode_id, S_ISREG)
285
def _set_mode(self, trans_id, mode_id, typefunc):
286
"""Set the mode of new file contents.
287
The mode_id is the existing file to get the mode from (often the same
288
as trans_id). The operation is only performed if there's a mode match
289
according to typefunc.
294
old_path = self._tree_id_paths[mode_id]
298
mode = os.stat(self._tree.abspath(old_path)).st_mode
300
if e.errno == errno.ENOENT:
305
os.chmod(self._limbo_name(trans_id), mode)
307
def create_directory(self, trans_id):
308
"""Schedule creation of a new directory.
310
See also new_directory.
312
os.mkdir(self._limbo_name(trans_id))
313
unique_add(self._new_contents, trans_id, 'directory')
315
def create_symlink(self, target, trans_id):
316
"""Schedule creation of a new symbolic link.
318
target is a bytestring.
319
See also new_symlink.
321
os.symlink(target, self._limbo_name(trans_id))
322
unique_add(self._new_contents, trans_id, 'symlink')
324
def cancel_creation(self, trans_id):
325
"""Cancel the creation of new file contents."""
326
del self._new_contents[trans_id]
327
delete_any(self._limbo_name(trans_id))
329
def delete_contents(self, trans_id):
330
"""Schedule the contents of a path entry for deletion"""
331
self.tree_kind(trans_id)
332
self._removed_contents.add(trans_id)
334
def cancel_deletion(self, trans_id):
335
"""Cancel a scheduled deletion"""
336
self._removed_contents.remove(trans_id)
338
def unversion_file(self, trans_id):
339
"""Schedule a path entry to become unversioned"""
340
self._removed_id.add(trans_id)
342
def delete_versioned(self, trans_id):
343
"""Delete and unversion a versioned file"""
344
self.delete_contents(trans_id)
345
self.unversion_file(trans_id)
347
def set_executability(self, executability, trans_id):
348
"""Schedule setting of the 'execute' bit
349
To unschedule, set to None
351
if executability is None:
352
del self._new_executability[trans_id]
354
unique_add(self._new_executability, trans_id, executability)
356
def version_file(self, file_id, trans_id):
357
"""Schedule a file to become versioned."""
358
assert file_id is not None
359
unique_add(self._new_id, trans_id, file_id)
360
unique_add(self._r_new_id, file_id, trans_id)
362
def cancel_versioning(self, trans_id):
363
"""Undo a previous versioning of a file"""
364
file_id = self._new_id[trans_id]
365
del self._new_id[trans_id]
366
del self._r_new_id[file_id]
369
"""Determine the paths of all new and changed files"""
371
fp = FinalPaths(self)
372
for id_set in (self._new_name, self._new_parent, self._new_contents,
373
self._new_id, self._new_executability):
374
new_ids.update(id_set)
375
new_paths = [(fp.get_path(t), t) for t in new_ids]
379
def tree_kind(self, trans_id):
380
"""Determine the file kind in the working tree.
382
Raises NoSuchFile if the file does not exist
384
path = self._tree_id_paths.get(trans_id)
386
raise NoSuchFile(None)
388
return file_kind(self._tree.abspath(path))
390
if e.errno != errno.ENOENT:
393
raise NoSuchFile(path)
395
def final_kind(self, trans_id):
396
"""Determine the final file kind, after any changes applied.
398
Raises NoSuchFile if the file does not exist/has no contents.
399
(It is conceivable that a path would be created without the
400
corresponding contents insertion command)
402
if trans_id in self._new_contents:
403
return self._new_contents[trans_id]
404
elif trans_id in self._removed_contents:
405
raise NoSuchFile(None)
407
return self.tree_kind(trans_id)
409
def tree_file_id(self, trans_id):
410
"""Determine the file id associated with the trans_id in the tree"""
412
path = self._tree_id_paths[trans_id]
414
# the file is a new, unversioned file, or invalid trans_id
416
# the file is old; the old id is still valid
417
if self._new_root == trans_id:
418
return self._tree.inventory.root.file_id
419
return self._tree.inventory.path2id(path)
421
def final_file_id(self, trans_id):
422
"""Determine the file id after any changes are applied, or None.
424
None indicates that the file will not be versioned after changes are
428
# there is a new id for this file
429
assert self._new_id[trans_id] is not None
430
return self._new_id[trans_id]
432
if trans_id in self._removed_id:
434
return self.tree_file_id(trans_id)
436
def inactive_file_id(self, trans_id):
437
"""Return the inactive file_id associated with a transaction id.
438
That is, the one in the tree or in non_present_ids.
439
The file_id may actually be active, too.
441
file_id = self.tree_file_id(trans_id)
442
if file_id is not None:
444
for key, value in self._non_present_ids.iteritems():
445
if value == trans_id:
448
def final_parent(self, trans_id):
449
"""Determine the parent file_id, after any changes are applied.
451
ROOT_PARENT is returned for the tree root.
454
return self._new_parent[trans_id]
456
return self.get_tree_parent(trans_id)
458
def final_name(self, trans_id):
459
"""Determine the final filename, after all changes are applied."""
461
return self._new_name[trans_id]
464
return os.path.basename(self._tree_id_paths[trans_id])
466
raise NoFinalPath(trans_id, self)
469
"""Return a map of parent: children for known parents.
471
Only new paths and parents of tree files with assigned ids are used.
474
items = list(self._new_parent.iteritems())
475
items.extend((t, self.final_parent(t)) for t in
476
self._tree_id_paths.keys())
477
for trans_id, parent_id in items:
478
if parent_id not in by_parent:
479
by_parent[parent_id] = set()
480
by_parent[parent_id].add(trans_id)
483
def path_changed(self, trans_id):
484
"""Return True if a trans_id's path has changed."""
485
return (trans_id in self._new_name) or (trans_id in self._new_parent)
487
def new_contents(self, trans_id):
488
return (trans_id in self._new_contents)
490
def find_conflicts(self):
491
"""Find any violations of inventory or filesystem invariants"""
492
if self.__done is True:
493
raise ReusingTransform()
495
# ensure all children of all existent parents are known
496
# all children of non-existent parents are known, by definition.
497
self._add_tree_children()
498
by_parent = self.by_parent()
499
conflicts.extend(self._unversioned_parents(by_parent))
500
conflicts.extend(self._parent_loops())
501
conflicts.extend(self._duplicate_entries(by_parent))
502
conflicts.extend(self._duplicate_ids())
503
conflicts.extend(self._parent_type_conflicts(by_parent))
504
conflicts.extend(self._improper_versioning())
505
conflicts.extend(self._executability_conflicts())
506
conflicts.extend(self._overwrite_conflicts())
509
def _add_tree_children(self):
510
"""Add all the children of all active parents to the known paths.
512
Active parents are those which gain children, and those which are
513
removed. This is a necessary first step in detecting conflicts.
515
parents = self.by_parent().keys()
516
parents.extend([t for t in self._removed_contents if
517
self.tree_kind(t) == 'directory'])
518
for trans_id in self._removed_id:
519
file_id = self.tree_file_id(trans_id)
520
if self._tree.inventory[file_id].kind == 'directory':
521
parents.append(trans_id)
523
for parent_id in parents:
524
# ensure that all children are registered with the transaction
525
list(self.iter_tree_children(parent_id))
527
def iter_tree_children(self, parent_id):
528
"""Iterate through the entry's tree children, if any"""
530
path = self._tree_id_paths[parent_id]
534
children = os.listdir(self._tree.abspath(path))
536
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
540
for child in children:
541
childpath = joinpath(path, child)
542
if self._tree.is_control_filename(childpath):
544
yield self.trans_id_tree_path(childpath)
546
def has_named_child(self, by_parent, parent_id, name):
548
children = by_parent[parent_id]
551
for child in children:
552
if self.final_name(child) == name:
555
path = self._tree_id_paths[parent_id]
558
childpath = joinpath(path, name)
559
child_id = self._tree_path_ids.get(childpath)
561
return lexists(self._tree.abspath(childpath))
563
if self.final_parent(child_id) != parent_id:
565
if child_id in self._removed_contents:
566
# XXX What about dangling file-ids?
571
def _parent_loops(self):
572
"""No entry should be its own ancestor"""
574
for trans_id in self._new_parent:
577
while parent_id is not ROOT_PARENT:
580
parent_id = self.final_parent(parent_id)
583
if parent_id == trans_id:
584
conflicts.append(('parent loop', trans_id))
585
if parent_id in seen:
589
def _unversioned_parents(self, by_parent):
590
"""If parent directories are versioned, children must be versioned."""
592
for parent_id, children in by_parent.iteritems():
593
if parent_id is ROOT_PARENT:
595
if self.final_file_id(parent_id) is not None:
597
for child_id in children:
598
if self.final_file_id(child_id) is not None:
599
conflicts.append(('unversioned parent', parent_id))
603
def _improper_versioning(self):
604
"""Cannot version a file with no contents, or a bad type.
606
However, existing entries with no contents are okay.
609
for trans_id in self._new_id.iterkeys():
611
kind = self.final_kind(trans_id)
613
conflicts.append(('versioning no contents', trans_id))
615
if not InventoryEntry.versionable_kind(kind):
616
conflicts.append(('versioning bad kind', trans_id, kind))
619
def _executability_conflicts(self):
620
"""Check for bad executability changes.
622
Only versioned files may have their executability set, because
623
1. only versioned entries can have executability under windows
624
2. only files can be executable. (The execute bit on a directory
625
does not indicate searchability)
628
for trans_id in self._new_executability:
629
if self.final_file_id(trans_id) is None:
630
conflicts.append(('unversioned executability', trans_id))
633
non_file = self.final_kind(trans_id) != "file"
637
conflicts.append(('non-file executability', trans_id))
640
def _overwrite_conflicts(self):
641
"""Check for overwrites (not permitted on Win32)"""
643
for trans_id in self._new_contents:
645
self.tree_kind(trans_id)
648
if trans_id not in self._removed_contents:
649
conflicts.append(('overwrite', trans_id,
650
self.final_name(trans_id)))
653
def _duplicate_entries(self, by_parent):
654
"""No directory may have two entries with the same name."""
656
for children in by_parent.itervalues():
657
name_ids = [(self.final_name(t), t) for t in children]
661
for name, trans_id in name_ids:
663
kind = self.final_kind(trans_id)
666
file_id = self.final_file_id(trans_id)
667
if kind is None and file_id is None:
669
if name == last_name:
670
conflicts.append(('duplicate', last_trans_id, trans_id,
673
last_trans_id = trans_id
676
def _duplicate_ids(self):
677
"""Each inventory id may only be used once"""
679
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
681
active_tree_ids = set((f for f in self._tree.inventory if
682
f not in removed_tree_ids))
683
for trans_id, file_id in self._new_id.iteritems():
684
if file_id in active_tree_ids:
685
old_trans_id = self.trans_id_tree_file_id(file_id)
686
conflicts.append(('duplicate id', old_trans_id, trans_id))
689
def _parent_type_conflicts(self, by_parent):
690
"""parents must have directory 'contents'."""
692
for parent_id, children in by_parent.iteritems():
693
if parent_id is ROOT_PARENT:
695
if not self._any_contents(children):
697
for child in children:
699
self.final_kind(child)
703
kind = self.final_kind(parent_id)
707
conflicts.append(('missing parent', parent_id))
708
elif kind != "directory":
709
conflicts.append(('non-directory parent', parent_id))
712
def _any_contents(self, trans_ids):
713
"""Return true if any of the trans_ids, will have contents."""
714
for trans_id in trans_ids:
716
kind = self.final_kind(trans_id)
723
"""Apply all changes to the inventory and filesystem.
725
If filesystem or inventory conflicts are present, MalformedTransform
728
conflicts = self.find_conflicts()
729
if len(conflicts) != 0:
730
raise MalformedTransform(conflicts=conflicts)
732
inv = self._tree.inventory
733
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
735
child_pb.update('Apply phase', 0, 2)
736
self._apply_removals(inv, limbo_inv)
737
child_pb.update('Apply phase', 1, 2)
738
modified_paths = self._apply_insertions(inv, limbo_inv)
741
self._tree._write_inventory(inv)
744
return _TransformResults(modified_paths)
746
def _limbo_name(self, trans_id):
747
"""Generate the limbo name of a file"""
748
return pathjoin(self._limbodir, trans_id)
750
def _apply_removals(self, inv, limbo_inv):
751
"""Perform tree operations that remove directory/inventory names.
753
That is, delete files that are to be deleted, and put any files that
754
need renaming into limbo. This must be done in strict child-to-parent
757
tree_paths = list(self._tree_path_ids.iteritems())
758
tree_paths.sort(reverse=True)
759
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
761
for num, data in enumerate(tree_paths):
762
path, trans_id = data
763
child_pb.update('removing file', num, len(tree_paths))
764
full_path = self._tree.abspath(path)
765
if trans_id in self._removed_contents:
766
delete_any(full_path)
767
elif trans_id in self._new_name or trans_id in \
770
os.rename(full_path, self._limbo_name(trans_id))
772
if e.errno != errno.ENOENT:
774
if trans_id in self._removed_id:
775
if trans_id == self._new_root:
776
file_id = self._tree.inventory.root.file_id
778
file_id = self.tree_file_id(trans_id)
780
elif trans_id in self._new_name or trans_id in self._new_parent:
781
file_id = self.tree_file_id(trans_id)
782
if file_id is not None:
783
limbo_inv[trans_id] = inv[file_id]
788
def _apply_insertions(self, inv, limbo_inv):
789
"""Perform tree operations that insert directory/inventory names.
791
That is, create any files that need to be created, and restore from
792
limbo any files that needed renaming. This must be done in strict
793
parent-to-child order.
795
new_paths = self.new_paths()
797
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
799
for num, (path, trans_id) in enumerate(new_paths):
800
child_pb.update('adding file', num, len(new_paths))
802
kind = self._new_contents[trans_id]
804
kind = contents = None
805
if trans_id in self._new_contents or \
806
self.path_changed(trans_id):
807
full_path = self._tree.abspath(path)
809
os.rename(self._limbo_name(trans_id), full_path)
811
# We may be renaming a dangling inventory id
812
if e.errno != errno.ENOENT:
814
if trans_id in self._new_contents:
815
modified_paths.append(full_path)
816
del self._new_contents[trans_id]
818
if trans_id in self._new_id:
820
kind = file_kind(self._tree.abspath(path))
821
inv.add_path(path, kind, self._new_id[trans_id])
822
elif trans_id in self._new_name or trans_id in\
824
entry = limbo_inv.get(trans_id)
825
if entry is not None:
826
entry.name = self.final_name(trans_id)
827
parent_path = os.path.dirname(path)
829
self._tree.inventory.path2id(parent_path)
832
# requires files and inventory entries to be in place
833
if trans_id in self._new_executability:
834
self._set_executability(path, inv, trans_id)
837
return modified_paths
839
def _set_executability(self, path, inv, trans_id):
840
"""Set the executability of versioned files """
841
file_id = inv.path2id(path)
842
new_executability = self._new_executability[trans_id]
843
inv[file_id].executable = new_executability
844
if supports_executable():
845
abspath = self._tree.abspath(path)
846
current_mode = os.stat(abspath).st_mode
847
if new_executability:
850
to_mode = current_mode | (0100 & ~umask)
851
# Enable x-bit for others only if they can read it.
852
if current_mode & 0004:
853
to_mode |= 0001 & ~umask
854
if current_mode & 0040:
855
to_mode |= 0010 & ~umask
857
to_mode = current_mode & ~0111
858
os.chmod(abspath, to_mode)
860
def _new_entry(self, name, parent_id, file_id):
861
"""Helper function to create a new filesystem entry."""
862
trans_id = self.create_path(name, parent_id)
863
if file_id is not None:
864
self.version_file(file_id, trans_id)
867
def new_file(self, name, parent_id, contents, file_id=None,
869
"""Convenience method to create files.
871
name is the name of the file to create.
872
parent_id is the transaction id of the parent directory of the file.
873
contents is an iterator of bytestrings, which will be used to produce
875
:param file_id: The inventory ID of the file, if it is to be versioned.
876
:param executable: Only valid when a file_id has been supplied.
878
trans_id = self._new_entry(name, parent_id, file_id)
879
# TODO: rather than scheduling a set_executable call,
880
# have create_file create the file with the right mode.
881
self.create_file(contents, trans_id)
882
if executable is not None:
883
self.set_executability(executable, trans_id)
886
def new_directory(self, name, parent_id, file_id=None):
887
"""Convenience method to create directories.
889
name is the name of the directory to create.
890
parent_id is the transaction id of the parent directory of the
892
file_id is the inventory ID of the directory, if it is to be versioned.
894
trans_id = self._new_entry(name, parent_id, file_id)
895
self.create_directory(trans_id)
898
def new_symlink(self, name, parent_id, target, file_id=None):
899
"""Convenience method to create symbolic link.
901
name is the name of the symlink to create.
902
parent_id is the transaction id of the parent directory of the symlink.
903
target is a bytestring of the target of the symlink.
904
file_id is the inventory ID of the file, if it is to be versioned.
906
trans_id = self._new_entry(name, parent_id, file_id)
907
self.create_symlink(target, trans_id)
910
def _affected_ids(self):
911
"""Return the set of transform ids affected by the transform"""
912
trans_ids = set(self._removed_id)
913
trans_ids.update(self._new_id.keys())
914
trans_ids.update(self._removed_contents)
915
trans_ids.update(self._new_contents.keys())
916
trans_ids.update(self._new_executability.keys())
917
trans_ids.update(self._new_name.keys())
918
trans_ids.update(self._new_parent.keys())
921
def _get_file_id_maps(self):
922
"""Return mapping of file_ids to trans_ids in the to and from states"""
923
trans_ids = self._affected_ids()
926
# Build up two dicts: trans_ids associated with file ids in the
927
# FROM state, vs the TO state.
928
for trans_id in trans_ids:
929
from_file_id = self.tree_file_id(trans_id)
930
if from_file_id is not None:
931
from_trans_ids[from_file_id] = trans_id
932
to_file_id = self.final_file_id(trans_id)
933
if to_file_id is not None:
934
to_trans_ids[to_file_id] = trans_id
935
return from_trans_ids, to_trans_ids
937
def _from_file_data(self, from_trans_id, from_versioned, file_id):
938
"""Get data about a file in the from (tree) state
940
Return a (name, parent, kind, executable) tuple
942
from_path = self._tree_id_paths.get(from_trans_id)
944
# get data from working tree if versioned
945
from_entry = self._tree.inventory[file_id]
946
from_name = from_entry.name
947
from_parent = from_entry.parent_id
950
if from_path is None:
951
# File does not exist in FROM state
955
# File exists, but is not versioned. Have to use path-
957
from_name = os.path.basename(from_path)
958
tree_parent = self.get_tree_parent(from_trans_id)
959
from_parent = self.tree_file_id(tree_parent)
960
if from_path is not None:
961
from_kind, from_executable, from_stats = \
962
self._tree._comparison_data(from_entry, from_path)
965
from_executable = False
966
return from_name, from_parent, from_kind, from_executable
968
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
969
"""Get data about a file in the to (target) state
971
Return a (name, parent, kind, executable) tuple
973
to_name = self.final_name(to_trans_id)
975
to_kind = self.final_kind(to_trans_id)
978
to_parent = self.final_file_id(self.final_parent(to_trans_id))
979
if to_trans_id in self._new_executability:
980
to_executable = self._new_executability[to_trans_id]
981
elif to_trans_id == from_trans_id:
982
to_executable = from_executable
984
to_executable = False
985
return to_name, to_parent, to_kind, to_executable
987
def _iter_changes(self):
988
"""Produce output in the same format as Tree._iter_changes.
990
Will produce nonsensical results if invoked while inventory/filesystem
991
conflicts (as reported by TreeTransform.find_conflicts()) are present.
993
This reads the Transform, but only reproduces changes involving a
994
file_id. Files that are not versioned in either of the FROM or TO
995
states are not reflected.
997
final_paths = FinalPaths(self)
998
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1000
# Now iterate through all active file_ids
1001
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1003
from_trans_id = from_trans_ids.get(file_id)
1004
# find file ids, and determine versioning state
1005
if from_trans_id is None:
1006
from_versioned = False
1007
from_trans_id = to_trans_ids[file_id]
1009
from_versioned = True
1010
to_trans_id = to_trans_ids.get(file_id)
1011
if to_trans_id is None:
1012
to_versioned = False
1013
to_trans_id = from_trans_id
1017
from_name, from_parent, from_kind, from_executable = \
1018
self._from_file_data(from_trans_id, from_versioned, file_id)
1020
to_name, to_parent, to_kind, to_executable = \
1021
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1023
to_path = final_paths.get_path(to_trans_id)
1024
if from_kind != to_kind:
1026
elif to_kind in ('file' or 'symlink') and (
1027
to_trans_id != from_trans_id or
1028
to_trans_id in self._new_contents):
1030
if (not modified and from_versioned == to_versioned and
1031
from_parent==to_parent and from_name == to_name and
1032
from_executable == to_executable):
1034
results.append((file_id, to_path, modified,
1035
(from_versioned, to_versioned),
1036
(from_parent, to_parent),
1037
(from_name, to_name),
1038
(from_kind, to_kind),
1039
(from_executable, to_executable)))
1040
return iter(sorted(results, key=lambda x:x[1]))
1043
def joinpath(parent, child):
1044
"""Join tree-relative paths, handling the tree root specially"""
1045
if parent is None or parent == "":
1048
return pathjoin(parent, child)
1051
class FinalPaths(object):
1052
"""Make path calculation cheap by memoizing paths.
1054
The underlying tree must not be manipulated between calls, or else
1055
the results will likely be incorrect.
1057
def __init__(self, transform):
1058
object.__init__(self)
1059
self._known_paths = {}
1060
self.transform = transform
1062
def _determine_path(self, trans_id):
1063
if trans_id == self.transform.root:
1065
name = self.transform.final_name(trans_id)
1066
parent_id = self.transform.final_parent(trans_id)
1067
if parent_id == self.transform.root:
1070
return pathjoin(self.get_path(parent_id), name)
1072
def get_path(self, trans_id):
1073
"""Find the final path associated with a trans_id"""
1074
if trans_id not in self._known_paths:
1075
self._known_paths[trans_id] = self._determine_path(trans_id)
1076
return self._known_paths[trans_id]
1078
def topology_sorted_ids(tree):
1079
"""Determine the topological order of the ids in a tree"""
1080
file_ids = list(tree)
1081
file_ids.sort(key=tree.id2path)
1085
def build_tree(tree, wt):
1086
"""Create working tree for a branch, using a TreeTransform.
1088
This function should be used on empty trees, having a tree root at most.
1089
(see merge and revert functionality for working with existing trees)
1091
Existing files are handled like so:
1093
- Existing bzrdirs take precedence over creating new items. They are
1094
created as '%s.diverted' % name.
1095
- Otherwise, if the content on disk matches the content we are building,
1096
it is silently replaced.
1097
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1099
if len(wt.inventory) > 1: # more than just a root
1100
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1102
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1103
pp = ProgressPhase("Build phase", 2, top_pb)
1104
# if tree.inventory.root is not None:
1105
# wt.set_root_id(tree.inventory.root.file_id)
1106
tt = TreeTransform(wt)
1110
file_trans_id[wt.get_root_id()] = \
1111
tt.trans_id_tree_file_id(wt.get_root_id())
1112
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1114
for num, (tree_path, entry) in \
1115
enumerate(tree.inventory.iter_entries_by_dir()):
1116
pb.update("Building tree", num, len(tree.inventory))
1117
if entry.parent_id is None:
1120
file_id = entry.file_id
1121
target_path = wt.abspath(tree_path)
1123
kind = file_kind(target_path)
1127
if kind == "directory":
1129
bzrdir.BzrDir.open(target_path)
1130
except errors.NotBranchError:
1134
if (file_id not in divert and
1135
_content_match(tree, entry, file_id, kind,
1137
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1138
if kind == 'directory':
1140
if entry.parent_id not in file_trans_id:
1141
raise repr(entry.parent_id)
1142
parent_id = file_trans_id[entry.parent_id]
1143
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1146
new_trans_id = file_trans_id[file_id]
1147
old_parent = tt.trans_id_tree_path(tree_path)
1148
_reparent_children(tt, old_parent, new_trans_id)
1152
divert_trans = set(file_trans_id[f] for f in divert)
1153
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1154
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1155
conflicts = cook_conflicts(raw_conflicts, tt)
1156
for conflict in conflicts:
1159
wt.add_conflicts(conflicts)
1160
except errors.UnsupportedOperation:
1168
def _reparent_children(tt, old_parent, new_parent):
1169
for child in tt.iter_tree_children(old_parent):
1170
tt.adjust_path(tt.final_name(child), new_parent, child)
1173
def _content_match(tree, entry, file_id, kind, target_path):
1174
if entry.kind != kind:
1176
if entry.kind == "directory":
1178
if entry.kind == "file":
1179
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1181
elif entry.kind == "symlink":
1182
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1187
def resolve_checkout(tt, conflicts, divert):
1188
new_conflicts = set()
1189
for c_type, conflict in ((c[0], c) for c in conflicts):
1190
# Anything but a 'duplicate' would indicate programmer error
1191
assert c_type == 'duplicate', c_type
1192
# Now figure out which is new and which is old
1193
if tt.new_contents(conflict[1]):
1194
new_file = conflict[1]
1195
old_file = conflict[2]
1197
new_file = conflict[2]
1198
old_file = conflict[1]
1200
# We should only get here if the conflict wasn't completely
1202
final_parent = tt.final_parent(old_file)
1203
if new_file in divert:
1204
new_name = tt.final_name(old_file)+'.diverted'
1205
tt.adjust_path(new_name, final_parent, new_file)
1206
new_conflicts.add((c_type, 'Diverted to',
1207
new_file, old_file))
1209
new_name = tt.final_name(old_file)+'.moved'
1210
tt.adjust_path(new_name, final_parent, old_file)
1211
new_conflicts.add((c_type, 'Moved existing file to',
1212
old_file, new_file))
1213
return new_conflicts
1216
def new_by_entry(tt, entry, parent_id, tree):
1217
"""Create a new file according to its inventory entry"""
1221
contents = tree.get_file(entry.file_id).readlines()
1222
executable = tree.is_executable(entry.file_id)
1223
return tt.new_file(name, parent_id, contents, entry.file_id,
1225
elif kind == 'directory':
1226
return tt.new_directory(name, parent_id, entry.file_id)
1227
elif kind == 'symlink':
1228
target = tree.get_symlink_target(entry.file_id)
1229
return tt.new_symlink(name, parent_id, target, entry.file_id)
1231
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1232
"""Create new file contents according to an inventory entry."""
1233
if entry.kind == "file":
1235
lines = tree.get_file(entry.file_id).readlines()
1236
tt.create_file(lines, trans_id, mode_id=mode_id)
1237
elif entry.kind == "symlink":
1238
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1239
elif entry.kind == "directory":
1240
tt.create_directory(trans_id)
1242
def create_entry_executability(tt, entry, trans_id):
1243
"""Set the executability of a trans_id according to an inventory entry"""
1244
if entry.kind == "file":
1245
tt.set_executability(entry.executable, trans_id)
1248
def find_interesting(working_tree, target_tree, filenames):
1249
"""Find the ids corresponding to specified filenames."""
1250
trees = (working_tree, target_tree)
1251
return tree.find_ids_across_trees(filenames, trees)
1254
def change_entry(tt, file_id, working_tree, target_tree,
1255
trans_id_file_id, backups, trans_id, by_parent):
1256
"""Replace a file_id's contents with those from a target tree."""
1257
e_trans_id = trans_id_file_id(file_id)
1258
entry = target_tree.inventory[file_id]
1259
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1262
mode_id = e_trans_id
1265
tt.delete_contents(e_trans_id)
1267
parent_trans_id = trans_id_file_id(entry.parent_id)
1268
backup_name = get_backup_name(entry, by_parent,
1269
parent_trans_id, tt)
1270
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1271
tt.unversion_file(e_trans_id)
1272
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1273
tt.version_file(file_id, e_trans_id)
1274
trans_id[file_id] = e_trans_id
1275
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1276
create_entry_executability(tt, entry, e_trans_id)
1279
tt.set_executability(entry.executable, e_trans_id)
1280
if tt.final_name(e_trans_id) != entry.name:
1283
parent_id = tt.final_parent(e_trans_id)
1284
parent_file_id = tt.final_file_id(parent_id)
1285
if parent_file_id != entry.parent_id:
1290
parent_trans_id = trans_id_file_id(entry.parent_id)
1291
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1294
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1295
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1298
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1299
"""Produce a backup-style name that appears to be available"""
1303
yield "%s.~%d~" % (name, counter)
1305
for new_name in name_gen():
1306
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1310
def _entry_changes(file_id, entry, working_tree):
1311
"""Determine in which ways the inventory entry has changed.
1313
Returns booleans: has_contents, content_mod, meta_mod
1314
has_contents means there are currently contents, but they differ
1315
contents_mod means contents need to be modified
1316
meta_mod means the metadata needs to be modified
1318
cur_entry = working_tree.inventory[file_id]
1320
working_kind = working_tree.kind(file_id)
1323
has_contents = False
1326
if has_contents is True:
1327
if entry.kind != working_kind:
1328
contents_mod, meta_mod = True, False
1330
cur_entry._read_tree_state(working_tree.id2path(file_id),
1332
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1333
cur_entry._forget_tree_state()
1334
return has_contents, contents_mod, meta_mod
1337
def revert(working_tree, target_tree, filenames, backups=False,
1338
pb=DummyProgress(), change_reporter=None):
1339
"""Revert a working tree's contents to those of a target tree."""
1340
target_tree.lock_read()
1341
tt = TreeTransform(working_tree, pb)
1343
pp = ProgressPhase("Revert phase", 3, pb)
1345
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1347
_alter_files(working_tree, target_tree, tt, child_pb,
1352
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1354
raw_conflicts = resolve_conflicts(tt, child_pb)
1357
conflicts = cook_conflicts(raw_conflicts, tt)
1359
change_reporter = delta.ChangeReporter(working_tree.inventory)
1360
delta.report_changes(tt._iter_changes(), change_reporter)
1361
for conflict in conflicts:
1365
working_tree.set_merge_modified({})
1367
target_tree.unlock()
1373
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1375
merge_modified = working_tree.merge_modified()
1376
change_list = target_tree._iter_changes(working_tree,
1377
specific_files=specific_files, pb=pb)
1378
if target_tree.inventory.root is None:
1384
for id_num, (file_id, path, changed_content, versioned, parent, name,
1385
kind, executable) in enumerate(change_list):
1386
if skip_root and file_id[0] is not None and parent[0] is None:
1388
trans_id = tt.trans_id_file_id(file_id)
1391
keep_content = False
1392
if kind[0] == 'file' and (backups or kind[1] is None):
1393
wt_sha1 = working_tree.get_file_sha1(file_id)
1394
if merge_modified.get(file_id) != wt_sha1:
1395
# acquire the basis tree lazyily to prevent the expense
1396
# of accessing it when its not needed ? (Guessing, RBC,
1398
if basis_tree is None:
1399
basis_tree = working_tree.basis_tree()
1400
basis_tree.lock_read()
1401
if file_id in basis_tree:
1402
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1404
elif kind[1] is None and not versioned[1]:
1406
if kind[0] is not None:
1407
if not keep_content:
1408
tt.delete_contents(trans_id)
1409
elif kind[1] is not None:
1410
parent_trans_id = tt.trans_id_file_id(parent[0])
1411
by_parent = tt.by_parent()
1412
backup_name = _get_backup_name(name[0], by_parent,
1413
parent_trans_id, tt)
1414
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1415
new_trans_id = tt.create_path(name[0], parent_trans_id)
1416
if versioned == (True, True):
1417
tt.unversion_file(trans_id)
1418
tt.version_file(file_id, new_trans_id)
1419
# New contents should have the same unix perms as old
1422
trans_id = new_trans_id
1423
if kind[1] == 'directory':
1424
tt.create_directory(trans_id)
1425
elif kind[1] == 'symlink':
1426
tt.create_symlink(target_tree.get_symlink_target(file_id),
1428
elif kind[1] == 'file':
1429
tt.create_file(target_tree.get_file_lines(file_id),
1431
# preserve the execute bit when backing up
1432
if keep_content and executable[0] == executable[1]:
1433
tt.set_executability(executable[1], trans_id)
1435
assert kind[1] is None
1436
if versioned == (False, True):
1437
tt.version_file(file_id, trans_id)
1438
if versioned == (True, False):
1439
tt.unversion_file(trans_id)
1440
if (name[1] is not None and
1441
(name[0] != name[1] or parent[0] != parent[1])):
1443
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1444
if executable[0] != executable[1] and kind[1] == "file":
1445
tt.set_executability(executable[1], trans_id)
1447
if basis_tree is not None:
1451
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1452
"""Make many conflict-resolution attempts, but die if they fail"""
1453
if pass_func is None:
1454
pass_func = conflict_pass
1455
new_conflicts = set()
1458
pb.update('Resolution pass', n+1, 10)
1459
conflicts = tt.find_conflicts()
1460
if len(conflicts) == 0:
1461
return new_conflicts
1462
new_conflicts.update(pass_func(tt, conflicts))
1463
raise MalformedTransform(conflicts=conflicts)
1468
def conflict_pass(tt, conflicts):
1469
"""Resolve some classes of conflicts."""
1470
new_conflicts = set()
1471
for c_type, conflict in ((c[0], c) for c in conflicts):
1472
if c_type == 'duplicate id':
1473
tt.unversion_file(conflict[1])
1474
new_conflicts.add((c_type, 'Unversioned existing file',
1475
conflict[1], conflict[2], ))
1476
elif c_type == 'duplicate':
1477
# files that were renamed take precedence
1478
new_name = tt.final_name(conflict[1])+'.moved'
1479
final_parent = tt.final_parent(conflict[1])
1480
if tt.path_changed(conflict[1]):
1481
tt.adjust_path(new_name, final_parent, conflict[2])
1482
new_conflicts.add((c_type, 'Moved existing file to',
1483
conflict[2], conflict[1]))
1485
tt.adjust_path(new_name, final_parent, conflict[1])
1486
new_conflicts.add((c_type, 'Moved existing file to',
1487
conflict[1], conflict[2]))
1488
elif c_type == 'parent loop':
1489
# break the loop by undoing one of the ops that caused the loop
1491
while not tt.path_changed(cur):
1492
cur = tt.final_parent(cur)
1493
new_conflicts.add((c_type, 'Cancelled move', cur,
1494
tt.final_parent(cur),))
1495
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1497
elif c_type == 'missing parent':
1498
trans_id = conflict[1]
1500
tt.cancel_deletion(trans_id)
1501
new_conflicts.add(('deleting parent', 'Not deleting',
1504
tt.create_directory(trans_id)
1505
new_conflicts.add((c_type, 'Created directory', trans_id))
1506
elif c_type == 'unversioned parent':
1507
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1508
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1509
return new_conflicts
1512
def cook_conflicts(raw_conflicts, tt):
1513
"""Generate a list of cooked conflicts, sorted by file path"""
1514
from bzrlib.conflicts import Conflict
1515
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1516
return sorted(conflict_iter, key=Conflict.sort_key)
1519
def iter_cook_conflicts(raw_conflicts, tt):
1520
from bzrlib.conflicts import Conflict
1522
for conflict in raw_conflicts:
1523
c_type = conflict[0]
1524
action = conflict[1]
1525
modified_path = fp.get_path(conflict[2])
1526
modified_id = tt.final_file_id(conflict[2])
1527
if len(conflict) == 3:
1528
yield Conflict.factory(c_type, action=action, path=modified_path,
1529
file_id=modified_id)
1532
conflicting_path = fp.get_path(conflict[3])
1533
conflicting_id = tt.final_file_id(conflict[3])
1534
yield Conflict.factory(c_type, action=action, path=modified_path,
1535
file_id=modified_id,
1536
conflict_path=conflicting_path,
1537
conflict_file_id=conflicting_id)