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
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
23
ReusingTransform, NotVersionedError, CantMoveRoot,
24
ExistingLimbo, ImmortalLimbo)
25
from bzrlib.inventory import InventoryEntry
26
from bzrlib.osutils import file_kind, supports_executable, pathjoin
27
from bzrlib.trace import mutter, warning
30
ROOT_PARENT = "root-parent"
33
def unique_add(map, key, value):
35
raise DuplicateKey(key=key)
39
class TreeTransform(object):
40
"""Represent a tree transformation."""
41
def __init__(self, tree):
42
"""Note: a write lock is taken on the tree.
44
Use TreeTransform.finalize() to release the lock
48
self._tree.lock_write()
50
control_files = self._tree._control_files
51
self._limbodir = control_files.controlfilename('limbo')
53
os.mkdir(self._limbodir)
55
if e.errno == errno.EEXIST:
56
raise ExistingLimbo(self._limbodir)
64
self._new_contents = {}
65
self._removed_contents = set()
66
self._new_executability = {}
68
self._non_present_ids = {}
70
self._removed_id = set()
71
self._tree_path_ids = {}
72
self._tree_id_paths = {}
73
self._new_root = self.get_id_tree(tree.get_root_id())
79
root = property(__get_root)
82
"""Release the working tree lock, if held, clean up limbo dir."""
83
if self._tree is None:
86
for trans_id, kind in self._new_contents.iteritems():
87
path = self._limbo_name(trans_id)
88
if kind == "directory":
93
os.rmdir(self._limbodir)
95
# We don't especially care *why* the dir is immortal.
96
raise ImmortalLimbo(self._limbodir)
101
def _assign_id(self):
102
"""Produce a new tranform id"""
103
new_id = "new-%s" % self._id_number
107
def create_path(self, name, parent):
108
"""Assign a transaction id to a new path"""
109
trans_id = self._assign_id()
110
unique_add(self._new_name, trans_id, name)
111
unique_add(self._new_parent, trans_id, parent)
114
def adjust_path(self, name, parent, trans_id):
115
"""Change the path that is assigned to a transaction id."""
116
if trans_id == self._new_root:
118
self._new_name[trans_id] = name
119
self._new_parent[trans_id] = parent
121
def adjust_root_path(self, name, parent):
122
"""Emulate moving the root by moving all children, instead.
124
We do this by undoing the association of root's transaction id with the
125
current tree. This allows us to create a new directory with that
126
transaction id. We unversion the root directory and version the
127
physically new directory, and hope someone versions the tree root
130
old_root = self._new_root
131
old_root_file_id = self.final_file_id(old_root)
132
# force moving all children of root
133
for child_id in self.iter_tree_children(old_root):
134
if child_id != parent:
135
self.adjust_path(self.final_name(child_id),
136
self.final_parent(child_id), child_id)
137
file_id = self.final_file_id(child_id)
138
if file_id is not None:
139
self.unversion_file(child_id)
140
self.version_file(file_id, child_id)
142
# the physical root needs a new transaction id
143
self._tree_path_ids.pop("")
144
self._tree_id_paths.pop(old_root)
145
self._new_root = self.get_id_tree(self._tree.get_root_id())
146
if parent == old_root:
147
parent = self._new_root
148
self.adjust_path(name, parent, old_root)
149
self.create_directory(old_root)
150
self.version_file(old_root_file_id, old_root)
151
self.unversion_file(self._new_root)
153
def get_id_tree(self, inventory_id):
154
"""Determine the transaction id of a working tree file.
156
This reflects only files that already exist, not ones that will be
157
added by transactions.
159
path = self._tree.inventory.id2path(inventory_id)
160
return self.get_tree_path_id(path)
162
def get_trans_id(self, file_id):
163
"""Determine or set the transaction id associated with a file ID.
164
A new id is only created for file_ids that were never present. If
165
a transaction has been unversioned, it is deliberately still returned.
166
(this will likely lead to an unversioned parent conflict.)
168
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
169
return self._r_new_id[file_id]
170
elif file_id in self._tree.inventory:
171
return self.get_id_tree(file_id)
172
elif file_id in self._non_present_ids:
173
return self._non_present_ids[file_id]
175
trans_id = self._assign_id()
176
self._non_present_ids[file_id] = trans_id
179
def canonical_path(self, path):
180
"""Get the canonical tree-relative path"""
181
# don't follow final symlinks
182
dirname, basename = os.path.split(self._tree.abspath(path))
183
dirname = os.path.realpath(dirname)
184
return self._tree.relpath(pathjoin(dirname, basename))
186
def get_tree_path_id(self, path):
187
"""Determine (and maybe set) the transaction ID for a tree path."""
188
path = self.canonical_path(path)
189
if path not in self._tree_path_ids:
190
self._tree_path_ids[path] = self._assign_id()
191
self._tree_id_paths[self._tree_path_ids[path]] = path
192
return self._tree_path_ids[path]
194
def get_tree_parent(self, trans_id):
195
"""Determine id of the parent in the tree."""
196
path = self._tree_id_paths[trans_id]
199
return self.get_tree_path_id(os.path.dirname(path))
201
def create_file(self, contents, trans_id, mode_id=None):
202
"""Schedule creation of a new file.
206
Contents is an iterator of strings, all of which will be written
207
to the target destination.
209
New file takes the permissions of any existing file with that id,
210
unless mode_id is specified.
212
f = file(self._limbo_name(trans_id), 'wb')
213
unique_add(self._new_contents, trans_id, 'file')
214
for segment in contents:
217
self._set_mode(trans_id, mode_id, S_ISREG)
219
def _set_mode(self, trans_id, mode_id, typefunc):
220
"""Set the mode of new file contents.
221
The mode_id is the existing file to get the mode from (often the same
222
as trans_id). The operation is only performed if there's a mode match
223
according to typefunc.
228
old_path = self._tree_id_paths[mode_id]
232
mode = os.stat(old_path).st_mode
234
if e.errno == errno.ENOENT:
239
os.chmod(self._limbo_name(trans_id), mode)
241
def create_directory(self, trans_id):
242
"""Schedule creation of a new directory.
244
See also new_directory.
246
os.mkdir(self._limbo_name(trans_id))
247
unique_add(self._new_contents, trans_id, 'directory')
249
def create_symlink(self, target, trans_id):
250
"""Schedule creation of a new symbolic link.
252
target is a bytestring.
253
See also new_symlink.
255
os.symlink(target, self._limbo_name(trans_id))
256
unique_add(self._new_contents, trans_id, 'symlink')
259
def delete_any(full_path):
260
"""Delete a file or directory."""
264
# We may be renaming a dangling inventory id
265
if e.errno != errno.EISDIR and e.errno != errno.EACCES:
269
def cancel_creation(self, trans_id):
270
"""Cancel the creation of new file contents."""
271
del self._new_contents[trans_id]
272
self.delete_any(self._limbo_name(trans_id))
274
def delete_contents(self, trans_id):
275
"""Schedule the contents of a path entry for deletion"""
276
self.tree_kind(trans_id)
277
self._removed_contents.add(trans_id)
279
def cancel_deletion(self, trans_id):
280
"""Cancel a scheduled deletion"""
281
self._removed_contents.remove(trans_id)
283
def unversion_file(self, trans_id):
284
"""Schedule a path entry to become unversioned"""
285
self._removed_id.add(trans_id)
287
def delete_versioned(self, trans_id):
288
"""Delete and unversion a versioned file"""
289
self.delete_contents(trans_id)
290
self.unversion_file(trans_id)
292
def set_executability(self, executability, trans_id):
293
"""Schedule setting of the 'execute' bit
294
To unschedule, set to None
296
if executability is None:
297
del self._new_executability[trans_id]
299
unique_add(self._new_executability, trans_id, executability)
301
def version_file(self, file_id, trans_id):
302
"""Schedule a file to become versioned."""
303
assert file_id is not None
304
unique_add(self._new_id, trans_id, file_id)
305
unique_add(self._r_new_id, file_id, trans_id)
307
def cancel_versioning(self, trans_id):
308
"""Undo a previous versioning of a file"""
309
file_id = self._new_id[trans_id]
310
del self._new_id[trans_id]
311
del self._r_new_id[file_id]
314
"""Determine the paths of all new and changed files"""
316
fp = FinalPaths(self)
317
for id_set in (self._new_name, self._new_parent, self._new_contents,
318
self._new_id, self._new_executability):
319
new_ids.update(id_set)
320
new_paths = [(fp.get_path(t), t) for t in new_ids]
324
def tree_kind(self, trans_id):
325
"""Determine the file kind in the working tree.
327
Raises NoSuchFile if the file does not exist
329
path = self._tree_id_paths.get(trans_id)
331
raise NoSuchFile(None)
333
return file_kind(self._tree.abspath(path))
335
if e.errno != errno.ENOENT:
338
raise NoSuchFile(path)
340
def final_kind(self, trans_id):
341
"""Determine the final file kind, after any changes applied.
343
Raises NoSuchFile if the file does not exist/has no contents.
344
(It is conceivable that a path would be created without the
345
corresponding contents insertion command)
347
if trans_id in self._new_contents:
348
return self._new_contents[trans_id]
349
elif trans_id in self._removed_contents:
350
raise NoSuchFile(None)
352
return self.tree_kind(trans_id)
354
def get_tree_file_id(self, trans_id):
355
"""Determine the file id associated with the trans_id in the tree"""
357
path = self._tree_id_paths[trans_id]
359
# the file is a new, unversioned file, or invalid trans_id
361
# the file is old; the old id is still valid
362
if self._new_root == trans_id:
363
return self._tree.inventory.root.file_id
364
return self._tree.inventory.path2id(path)
366
def final_file_id(self, trans_id):
367
"""Determine the file id after any changes are applied, or None.
369
None indicates that the file will not be versioned after changes are
373
# there is a new id for this file
374
assert self._new_id[trans_id] is not None
375
return self._new_id[trans_id]
377
if trans_id in self._removed_id:
379
return self.get_tree_file_id(trans_id)
381
def inactive_file_id(self, trans_id):
382
"""Return the inactive file_id associated with a transaction id.
383
That is, the one in the tree or in non_present_ids.
384
The file_id may actually be active, too.
386
file_id = self.get_tree_file_id(trans_id)
387
if file_id is not None:
389
for key, value in self._non_present_ids.iteritems():
390
if value == trans_id:
393
def final_parent(self, trans_id):
394
"""Determine the parent file_id, after any changes are applied.
396
ROOT_PARENT is returned for the tree root.
399
return self._new_parent[trans_id]
401
return self.get_tree_parent(trans_id)
403
def final_name(self, trans_id):
404
"""Determine the final filename, after all changes are applied."""
406
return self._new_name[trans_id]
408
return os.path.basename(self._tree_id_paths[trans_id])
410
def _by_parent(self):
411
"""Return a map of parent: children for known parents.
413
Only new paths and parents of tree files with assigned ids are used.
416
items = list(self._new_parent.iteritems())
417
items.extend((t, self.final_parent(t)) for t in
418
self._tree_id_paths.keys())
419
for trans_id, parent_id in items:
420
if parent_id not in by_parent:
421
by_parent[parent_id] = set()
422
by_parent[parent_id].add(trans_id)
425
def path_changed(self, trans_id):
426
"""Return True if a trans_id's path has changed."""
427
return trans_id in self._new_name or trans_id in self._new_parent
429
def find_conflicts(self):
430
"""Find any violations of inventory or filesystem invariants"""
431
if self.__done is True:
432
raise ReusingTransform()
434
# ensure all children of all existent parents are known
435
# all children of non-existent parents are known, by definition.
436
self._add_tree_children()
437
by_parent = self._by_parent()
438
conflicts.extend(self._unversioned_parents(by_parent))
439
conflicts.extend(self._parent_loops())
440
conflicts.extend(self._duplicate_entries(by_parent))
441
conflicts.extend(self._duplicate_ids())
442
conflicts.extend(self._parent_type_conflicts(by_parent))
443
conflicts.extend(self._improper_versioning())
444
conflicts.extend(self._executability_conflicts())
445
conflicts.extend(self._overwrite_conflicts())
448
def _add_tree_children(self):
449
"""Add all the children of all active parents to the known paths.
451
Active parents are those which gain children, and those which are
452
removed. This is a necessary first step in detecting conflicts.
454
parents = self._by_parent().keys()
455
parents.extend([t for t in self._removed_contents if
456
self.tree_kind(t) == 'directory'])
457
for trans_id in self._removed_id:
458
file_id = self.get_tree_file_id(trans_id)
459
if self._tree.inventory[file_id].kind in ('directory',
461
parents.append(trans_id)
463
for parent_id in parents:
464
# ensure that all children are registered with the transaction
465
list(self.iter_tree_children(parent_id))
467
def iter_tree_children(self, parent_id):
468
"""Iterate through the entry's tree children, if any"""
470
path = self._tree_id_paths[parent_id]
474
children = os.listdir(self._tree.abspath(path))
476
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
480
for child in children:
481
childpath = joinpath(path, child)
482
if childpath == BZRDIR:
484
yield self.get_tree_path_id(childpath)
486
def _parent_loops(self):
487
"""No entry should be its own ancestor"""
489
for trans_id in self._new_parent:
492
while parent_id is not ROOT_PARENT:
494
parent_id = self.final_parent(parent_id)
495
if parent_id == trans_id:
496
conflicts.append(('parent loop', trans_id))
497
if parent_id in seen:
501
def _unversioned_parents(self, by_parent):
502
"""If parent directories are versioned, children must be versioned."""
504
for parent_id, children in by_parent.iteritems():
505
if parent_id is ROOT_PARENT:
507
if self.final_file_id(parent_id) is not None:
509
for child_id in children:
510
if self.final_file_id(child_id) is not None:
511
conflicts.append(('unversioned parent', parent_id))
515
def _improper_versioning(self):
516
"""Cannot version a file with no contents, or a bad type.
518
However, existing entries with no contents are okay.
521
for trans_id in self._new_id.iterkeys():
523
kind = self.final_kind(trans_id)
525
conflicts.append(('versioning no contents', trans_id))
527
if not InventoryEntry.versionable_kind(kind):
528
conflicts.append(('versioning bad kind', trans_id, kind))
531
def _executability_conflicts(self):
532
"""Check for bad executability changes.
534
Only versioned files may have their executability set, because
535
1. only versioned entries can have executability under windows
536
2. only files can be executable. (The execute bit on a directory
537
does not indicate searchability)
540
for trans_id in self._new_executability:
541
if self.final_file_id(trans_id) is None:
542
conflicts.append(('unversioned executability', trans_id))
545
non_file = self.final_kind(trans_id) != "file"
549
conflicts.append(('non-file executability', trans_id))
552
def _overwrite_conflicts(self):
553
"""Check for overwrites (not permitted on Win32)"""
555
for trans_id in self._new_contents:
557
self.tree_kind(trans_id)
560
if trans_id not in self._removed_contents:
561
conflicts.append(('overwrite', trans_id,
562
self.final_name(trans_id)))
565
def _duplicate_entries(self, by_parent):
566
"""No directory may have two entries with the same name."""
568
for children in by_parent.itervalues():
569
name_ids = [(self.final_name(t), t) for t in children]
573
for name, trans_id in name_ids:
574
if name == last_name:
575
conflicts.append(('duplicate', last_trans_id, trans_id,
578
last_trans_id = trans_id
581
def _duplicate_ids(self):
582
"""Each inventory id may only be used once"""
584
removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
586
active_tree_ids = set((f for f in self._tree.inventory if
587
f not in removed_tree_ids))
588
for trans_id, file_id in self._new_id.iteritems():
589
if file_id in active_tree_ids:
590
old_trans_id = self.get_id_tree(file_id)
591
conflicts.append(('duplicate id', old_trans_id, trans_id))
594
def _parent_type_conflicts(self, by_parent):
595
"""parents must have directory 'contents'."""
597
for parent_id, children in by_parent.iteritems():
598
if parent_id is ROOT_PARENT:
600
if not self._any_contents(children):
602
for child in children:
604
self.final_kind(child)
608
kind = self.final_kind(parent_id)
612
conflicts.append(('missing parent', parent_id))
613
elif kind != "directory":
614
conflicts.append(('non-directory parent', parent_id))
617
def _any_contents(self, trans_ids):
618
"""Return true if any of the trans_ids, will have contents."""
619
for trans_id in trans_ids:
621
kind = self.final_kind(trans_id)
628
"""Apply all changes to the inventory and filesystem.
630
If filesystem or inventory conflicts are present, MalformedTransform
633
conflicts = self.find_conflicts()
634
if len(conflicts) != 0:
635
raise MalformedTransform(conflicts=conflicts)
637
inv = self._tree.inventory
638
self._apply_removals(inv, limbo_inv)
639
self._apply_insertions(inv, limbo_inv)
640
self._tree._write_inventory(inv)
644
def _limbo_name(self, trans_id):
645
"""Generate the limbo name of a file"""
646
return pathjoin(self._limbodir, trans_id)
648
def _apply_removals(self, inv, limbo_inv):
649
"""Perform tree operations that remove directory/inventory names.
651
That is, delete files that are to be deleted, and put any files that
652
need renaming into limbo. This must be done in strict child-to-parent
655
tree_paths = list(self._tree_path_ids.iteritems())
656
tree_paths.sort(reverse=True)
657
for path, trans_id in tree_paths:
658
full_path = self._tree.abspath(path)
659
if trans_id in self._removed_contents:
660
self.delete_any(full_path)
661
elif trans_id in self._new_name or trans_id in self._new_parent:
663
os.rename(full_path, self._limbo_name(trans_id))
665
if e.errno != errno.ENOENT:
667
if trans_id in self._removed_id:
668
if trans_id == self._new_root:
669
file_id = self._tree.inventory.root.file_id
671
file_id = self.get_tree_file_id(trans_id)
673
elif trans_id in self._new_name or trans_id in self._new_parent:
674
file_id = self.get_tree_file_id(trans_id)
675
if file_id is not None:
676
limbo_inv[trans_id] = inv[file_id]
679
def _apply_insertions(self, inv, limbo_inv):
680
"""Perform tree operations that insert directory/inventory names.
682
That is, create any files that need to be created, and restore from
683
limbo any files that needed renaming. This must be done in strict
684
parent-to-child order.
686
for path, trans_id in self.new_paths():
688
kind = self._new_contents[trans_id]
690
kind = contents = None
691
if trans_id in self._new_contents or self.path_changed(trans_id):
692
full_path = self._tree.abspath(path)
694
os.rename(self._limbo_name(trans_id), full_path)
696
# We may be renaming a dangling inventory id
697
if e.errno != errno.ENOENT:
699
if trans_id in self._new_contents:
700
del self._new_contents[trans_id]
702
if trans_id in self._new_id:
704
kind = file_kind(self._tree.abspath(path))
705
inv.add_path(path, kind, self._new_id[trans_id])
706
elif trans_id in self._new_name or trans_id in self._new_parent:
707
entry = limbo_inv.get(trans_id)
708
if entry is not None:
709
entry.name = self.final_name(trans_id)
710
parent_path = os.path.dirname(path)
711
entry.parent_id = self._tree.inventory.path2id(parent_path)
714
# requires files and inventory entries to be in place
715
if trans_id in self._new_executability:
716
self._set_executability(path, inv, trans_id)
718
def _set_executability(self, path, inv, trans_id):
719
"""Set the executability of versioned files """
720
file_id = inv.path2id(path)
721
new_executability = self._new_executability[trans_id]
722
inv[file_id].executable = new_executability
723
if supports_executable():
724
abspath = self._tree.abspath(path)
725
current_mode = os.stat(abspath).st_mode
726
if new_executability:
729
to_mode = current_mode | (0100 & ~umask)
730
# Enable x-bit for others only if they can read it.
731
if current_mode & 0004:
732
to_mode |= 0001 & ~umask
733
if current_mode & 0040:
734
to_mode |= 0010 & ~umask
736
to_mode = current_mode & ~0111
737
os.chmod(abspath, to_mode)
739
def _new_entry(self, name, parent_id, file_id):
740
"""Helper function to create a new filesystem entry."""
741
trans_id = self.create_path(name, parent_id)
742
if file_id is not None:
743
self.version_file(file_id, trans_id)
746
def new_file(self, name, parent_id, contents, file_id=None,
748
"""Convenience method to create files.
750
name is the name of the file to create.
751
parent_id is the transaction id of the parent directory of the file.
752
contents is an iterator of bytestrings, which will be used to produce
754
file_id is the inventory ID of the file, if it is to be versioned.
756
trans_id = self._new_entry(name, parent_id, file_id)
757
self.create_file(contents, trans_id)
758
if executable is not None:
759
self.set_executability(executable, trans_id)
762
def new_directory(self, name, parent_id, file_id=None):
763
"""Convenience method to create directories.
765
name is the name of the directory to create.
766
parent_id is the transaction id of the parent directory of the
768
file_id is the inventory ID of the directory, if it is to be versioned.
770
trans_id = self._new_entry(name, parent_id, file_id)
771
self.create_directory(trans_id)
774
def new_symlink(self, name, parent_id, target, file_id=None):
775
"""Convenience method to create symbolic link.
777
name is the name of the symlink to create.
778
parent_id is the transaction id of the parent directory of the symlink.
779
target is a bytestring of the target of the symlink.
780
file_id is the inventory ID of the file, if it is to be versioned.
782
trans_id = self._new_entry(name, parent_id, file_id)
783
self.create_symlink(target, trans_id)
786
def joinpath(parent, child):
787
"""Join tree-relative paths, handling the tree root specially"""
788
if parent is None or parent == "":
791
return pathjoin(parent, child)
794
class FinalPaths(object):
795
"""Make path calculation cheap by memoizing paths.
797
The underlying tree must not be manipulated between calls, or else
798
the results will likely be incorrect.
800
def __init__(self, transform):
801
object.__init__(self)
802
self._known_paths = {}
803
self.transform = transform
805
def _determine_path(self, trans_id):
806
if trans_id == self.transform.root:
808
name = self.transform.final_name(trans_id)
809
parent_id = self.transform.final_parent(trans_id)
810
if parent_id == self.transform.root:
813
return pathjoin(self.get_path(parent_id), name)
815
def get_path(self, trans_id):
816
"""Find the final path associated with a trans_id"""
817
if trans_id not in self._known_paths:
818
self._known_paths[trans_id] = self._determine_path(trans_id)
819
return self._known_paths[trans_id]
821
def topology_sorted_ids(tree):
822
"""Determine the topological order of the ids in a tree"""
823
file_ids = list(tree)
824
file_ids.sort(key=tree.id2path)
827
def build_tree(tree, wt):
828
"""Create working tree for a branch, using a Transaction."""
830
tt = TreeTransform(wt)
832
file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
833
file_ids = topology_sorted_ids(tree)
834
for file_id in file_ids:
835
entry = tree.inventory[file_id]
836
if entry.parent_id is None:
838
if entry.parent_id not in file_trans_id:
839
raise repr(entry.parent_id)
840
parent_id = file_trans_id[entry.parent_id]
841
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
846
def new_by_entry(tt, entry, parent_id, tree):
847
"""Create a new file according to its inventory entry"""
851
contents = tree.get_file(entry.file_id).readlines()
852
executable = tree.is_executable(entry.file_id)
853
return tt.new_file(name, parent_id, contents, entry.file_id,
855
elif kind == 'directory':
856
return tt.new_directory(name, parent_id, entry.file_id)
857
elif kind == 'symlink':
858
target = entry.get_symlink_target(file_id)
859
return tt.new_symlink(name, parent_id, target, file_id)
861
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
862
"""Create new file contents according to an inventory entry."""
863
if entry.kind == "file":
865
lines = tree.get_file(entry.file_id).readlines()
866
tt.create_file(lines, trans_id, mode_id=mode_id)
867
elif entry.kind == "symlink":
868
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
869
elif entry.kind == "directory":
870
tt.create_directory(trans_id)
872
def create_entry_executability(tt, entry, trans_id):
873
"""Set the executability of a trans_id according to an inventory entry"""
874
if entry.kind == "file":
875
tt.set_executability(entry.executable, trans_id)
878
def find_interesting(working_tree, target_tree, filenames):
879
"""Find the ids corresponding to specified filenames."""
881
interesting_ids = None
883
interesting_ids = set()
884
for tree_path in filenames:
885
for tree in (working_tree, target_tree):
887
file_id = tree.inventory.path2id(tree_path)
888
if file_id is not None:
889
interesting_ids.add(file_id)
892
raise NotVersionedError(path=tree_path)
893
return interesting_ids
896
def change_entry(tt, file_id, working_tree, target_tree,
897
get_trans_id, backups, trans_id):
898
"""Replace a file_id's contents with those from a target tree."""
899
e_trans_id = get_trans_id(file_id)
900
entry = target_tree.inventory[file_id]
901
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
907
tt.delete_contents(e_trans_id)
909
parent_trans_id = get_trans_id(entry.parent_id)
910
tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
911
tt.unversion_file(e_trans_id)
912
e_trans_id = tt.create_path(entry.name, parent_trans_id)
913
tt.version_file(file_id, e_trans_id)
914
trans_id[file_id] = e_trans_id
915
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
916
create_entry_executability(tt, entry, e_trans_id)
919
tt.set_executability(entry.executable, e_trans_id)
920
if tt.final_name(e_trans_id) != entry.name:
923
parent_id = tt.final_parent(e_trans_id)
924
parent_file_id = tt.final_file_id(parent_id)
925
if parent_file_id != entry.parent_id:
930
parent_trans_id = get_trans_id(entry.parent_id)
931
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
934
def _entry_changes(file_id, entry, working_tree):
935
"""Determine in which ways the inventory entry has changed.
937
Returns booleans: has_contents, content_mod, meta_mod
938
has_contents means there are currently contents, but they differ
939
contents_mod means contents need to be modified
940
meta_mod means the metadata needs to be modified
942
cur_entry = working_tree.inventory[file_id]
944
working_kind = working_tree.kind(file_id)
947
if e.errno != errno.ENOENT:
952
if has_contents is True:
953
real_e_kind = entry.kind
954
if real_e_kind == 'root_directory':
955
real_e_kind = 'directory'
956
if real_e_kind != working_kind:
957
contents_mod, meta_mod = True, False
959
cur_entry._read_tree_state(working_tree.id2path(file_id),
961
contents_mod, meta_mod = entry.detect_changes(cur_entry)
962
cur_entry._forget_tree_state()
963
return has_contents, contents_mod, meta_mod
966
def revert(working_tree, target_tree, filenames, backups=False):
967
"""Revert a working tree's contents to those of a target tree."""
968
interesting_ids = find_interesting(working_tree, target_tree, filenames)
969
def interesting(file_id):
970
return interesting_ids is None or file_id in interesting_ids
972
tt = TreeTransform(working_tree)
975
def get_trans_id(file_id):
977
return trans_id[file_id]
979
return tt.get_id_tree(file_id)
981
for file_id in topology_sorted_ids(target_tree):
982
if not interesting(file_id):
984
if file_id not in working_tree.inventory:
985
entry = target_tree.inventory[file_id]
986
parent_id = get_trans_id(entry.parent_id)
987
e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
988
trans_id[file_id] = e_trans_id
990
change_entry(tt, file_id, working_tree, target_tree,
991
get_trans_id, backups, trans_id)
992
for file_id in working_tree.inventory:
993
if not interesting(file_id):
995
if file_id not in target_tree:
996
tt.unversion_file(tt.get_id_tree(file_id))
997
raw_conflicts = resolve_conflicts(tt)
998
for line in conflicts_strings(cook_conflicts(raw_conflicts, tt)):
1005
def resolve_conflicts(tt):
1006
"""Make many conflict-resolution attempts, but die if they fail"""
1007
new_conflicts = set()
1009
conflicts = tt.find_conflicts()
1010
if len(conflicts) == 0:
1011
return new_conflicts
1012
new_conflicts.update(conflict_pass(tt, conflicts))
1013
raise MalformedTransform(conflicts=conflicts)
1016
def conflict_pass(tt, conflicts):
1017
"""Resolve some classes of conflicts."""
1018
new_conflicts = set()
1019
for c_type, conflict in ((c[0], c) for c in conflicts):
1020
if c_type == 'duplicate id':
1021
tt.unversion_file(conflict[1])
1022
new_conflicts.add((c_type, 'Unversioned existing file',
1023
conflict[1], conflict[2], ))
1024
elif c_type == 'duplicate':
1025
# files that were renamed take precedence
1026
new_name = tt.final_name(conflict[1])+'.moved'
1027
final_parent = tt.final_parent(conflict[1])
1028
if tt.path_changed(conflict[1]):
1029
tt.adjust_path(new_name, final_parent, conflict[2])
1030
new_conflicts.add((c_type, 'Moved existing file to',
1031
conflict[2], conflict[1]))
1033
tt.adjust_path(new_name, final_parent, conflict[1])
1034
new_conflicts.add((c_type, 'Moved existing file to',
1035
conflict[1], conflict[2]))
1036
elif c_type == 'parent loop':
1037
# break the loop by undoing one of the ops that caused the loop
1039
while not tt.path_changed(cur):
1040
cur = tt.final_parent(cur)
1041
new_conflicts.add((c_type, 'Cancelled move', cur,
1042
tt.final_parent(cur),))
1043
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1045
elif c_type == 'missing parent':
1046
trans_id = conflict[1]
1048
tt.cancel_deletion(trans_id)
1049
new_conflicts.add((c_type, 'Not deleting', trans_id))
1051
tt.create_directory(trans_id)
1052
new_conflicts.add((c_type, 'Created directory.', trans_id))
1053
elif c_type == 'unversioned parent':
1054
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1055
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1056
return new_conflicts
1058
def cook_conflicts(raw_conflicts, tt):
1059
"""Generate a list of cooked conflicts, sorted by file path"""
1061
if conflict[2] is not None:
1062
return conflict[2], conflict[0]
1063
elif len(conflict) == 6:
1064
return conflict[4], conflict[0]
1066
return None, conflict[0]
1068
return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1070
def iter_cook_conflicts(raw_conflicts, tt):
1071
cooked_conflicts = []
1073
for conflict in raw_conflicts:
1074
c_type = conflict[0]
1075
action = conflict[1]
1076
modified_path = fp.get_path(conflict[2])
1077
modified_id = tt.final_file_id(conflict[2])
1078
if len(conflict) == 3:
1079
yield c_type, action, modified_path, modified_id
1081
conflicting_path = fp.get_path(conflict[3])
1082
conflicting_id = tt.final_file_id(conflict[3])
1083
yield (c_type, action, modified_path, modified_id,
1084
conflicting_path, conflicting_id)
1087
def conflicts_strings(conflicts):
1088
"""Generate strings for the provided conflicts"""
1089
for conflict in conflicts:
1090
conflict_type = conflict[0]
1091
if conflict_type == 'text conflict':
1092
yield 'Text conflict in %s' % conflict[2]
1093
elif conflict_type == 'contents conflict':
1094
yield 'Contents conflict in %s' % conflict[2]
1095
elif conflict_type == 'path conflict':
1096
yield 'Path conflict: %s / %s' % conflict[2:]
1097
elif conflict_type == 'duplicate id':
1098
vals = (conflict[4], conflict[1], conflict[2])
1099
yield 'Conflict adding id to %s. %s %s.' % vals
1100
elif conflict_type == 'duplicate':
1101
vals = (conflict[4], conflict[1], conflict[2])
1102
yield 'Conflict adding file %s. %s %s.' % vals
1103
elif conflict_type == 'parent loop':
1104
vals = (conflict[4], conflict[2], conflict[1])
1105
yield 'Conflict moving %s into %s. %s.' % vals
1106
elif conflict_type == 'unversioned parent':
1107
vals = (conflict[2], conflict[1])
1108
yield 'Conflict adding versioned files to %s. %s.' % vals
1109
elif conflict_type == 'missing parent':
1110
vals = (conflict[2], conflict[1])
1111
yield 'Conflict adding files to %s. %s.' % vals