Merge lp:~jelmer/brz/transform into lp:brz/3.1

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/transform
Merge into: lp:brz/3.1
Diff against target: 3993 lines (+2475/-1235)
6 files modified
breezy/bzr/transform.py (+1195/-2)
breezy/git/transform.py (+1195/-3)
breezy/git/tree.py (+1/-1)
breezy/tests/per_tree/test_transform.py (+70/-43)
breezy/tests/per_workingtree/test_transform.py (+14/-0)
breezy/transform.py (+0/-1186)
To merge this branch: bzr merge lp:~jelmer/brz/transform
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+386867@code.launchpad.net

Commit message

Split out git and bzr-specific transform.

Description of the change

Split out git and bzr-specific transform.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/bzr/transform.py'
2--- breezy/bzr/transform.py 2020-07-05 19:02:14 +0000
3+++ breezy/bzr/transform.py 2020-07-06 02:29:08 +0000
4@@ -19,14 +19,17 @@
5
6 import errno
7 import os
8-from stat import S_IEXEC
9+from stat import S_IEXEC, S_ISREG
10+import time
11
12 from .. import (
13 annotate,
14 errors,
15 lock,
16+ multiparent,
17 osutils,
18 revision as _mod_revision,
19+ trace,
20 tree,
21 ui,
22 urlutils,
23@@ -39,19 +42,1209 @@
24 ROOT_PARENT,
25 _FileMover,
26 _TransformResults,
27- DiskTreeTransform,
28 joinpath,
29 NoFinalPath,
30 FinalPaths,
31 unique_add,
32+ TreeTransform,
33 TransformRenameFailed,
34+ ImmortalLimbo,
35+ ReusingTransform,
36+ MalformedTransform,
37 )
38+from ..tree import TreeChange
39 from . import (
40 inventory,
41 inventorytree,
42 )
43
44
45+class TreeTransformBase(TreeTransform):
46+ """The base class for TreeTransform and its kin."""
47+
48+ def __init__(self, tree, pb=None, case_sensitive=True):
49+ """Constructor.
50+
51+ :param tree: The tree that will be transformed, but not necessarily
52+ the output tree.
53+ :param pb: ignored
54+ :param case_sensitive: If True, the target of the transform is
55+ case sensitive, not just case preserving.
56+ """
57+ super(TreeTransformBase, self).__init__(tree, pb=pb)
58+ # mapping of trans_id => (sha1 of content, stat_value)
59+ self._observed_sha1s = {}
60+ # Mapping of trans_id -> new file_id
61+ self._new_id = {}
62+ # Mapping of old file-id -> trans_id
63+ self._non_present_ids = {}
64+ # Mapping of new file_id -> trans_id
65+ self._r_new_id = {}
66+ # The trans_id that will be used as the tree root
67+ if tree.is_versioned(''):
68+ self._new_root = self.trans_id_tree_path('')
69+ else:
70+ self._new_root = None
71+ # Whether the target is case sensitive
72+ self._case_sensitive_target = case_sensitive
73+
74+ def finalize(self):
75+ """Release the working tree lock, if held.
76+
77+ This is required if apply has not been invoked, but can be invoked
78+ even after apply.
79+ """
80+ if self._tree is None:
81+ return
82+ for hook in MutableTree.hooks['post_transform']:
83+ hook(self._tree, self)
84+ self._tree.unlock()
85+ self._tree = None
86+
87+ def __get_root(self):
88+ return self._new_root
89+
90+ root = property(__get_root)
91+
92+ def create_path(self, name, parent):
93+ """Assign a transaction id to a new path"""
94+ trans_id = self._assign_id()
95+ unique_add(self._new_name, trans_id, name)
96+ unique_add(self._new_parent, trans_id, parent)
97+ return trans_id
98+
99+ def adjust_root_path(self, name, parent):
100+ """Emulate moving the root by moving all children, instead.
101+
102+ We do this by undoing the association of root's transaction id with the
103+ current tree. This allows us to create a new directory with that
104+ transaction id. We unversion the root directory and version the
105+ physically new directory, and hope someone versions the tree root
106+ later.
107+ """
108+ old_root = self._new_root
109+ old_root_file_id = self.final_file_id(old_root)
110+ # force moving all children of root
111+ for child_id in self.iter_tree_children(old_root):
112+ if child_id != parent:
113+ self.adjust_path(self.final_name(child_id),
114+ self.final_parent(child_id), child_id)
115+ file_id = self.final_file_id(child_id)
116+ if file_id is not None:
117+ self.unversion_file(child_id)
118+ self.version_file(child_id, file_id=file_id)
119+
120+ # the physical root needs a new transaction id
121+ self._tree_path_ids.pop("")
122+ self._tree_id_paths.pop(old_root)
123+ self._new_root = self.trans_id_tree_path('')
124+ if parent == old_root:
125+ parent = self._new_root
126+ self.adjust_path(name, parent, old_root)
127+ self.create_directory(old_root)
128+ self.version_file(old_root, file_id=old_root_file_id)
129+ self.unversion_file(self._new_root)
130+
131+ def fixup_new_roots(self):
132+ """Reinterpret requests to change the root directory
133+
134+ Instead of creating a root directory, or moving an existing directory,
135+ all the attributes and children of the new root are applied to the
136+ existing root directory.
137+
138+ This means that the old root trans-id becomes obsolete, so it is
139+ recommended only to invoke this after the root trans-id has become
140+ irrelevant.
141+
142+ """
143+ new_roots = [k for k, v in viewitems(self._new_parent)
144+ if v == ROOT_PARENT]
145+ if len(new_roots) < 1:
146+ return
147+ if len(new_roots) != 1:
148+ raise ValueError('A tree cannot have two roots!')
149+ if self._new_root is None:
150+ self._new_root = new_roots[0]
151+ return
152+ old_new_root = new_roots[0]
153+ # unversion the new root's directory.
154+ if self.final_kind(self._new_root) is None:
155+ file_id = self.final_file_id(old_new_root)
156+ else:
157+ file_id = self.final_file_id(self._new_root)
158+ if old_new_root in self._new_id:
159+ self.cancel_versioning(old_new_root)
160+ else:
161+ self.unversion_file(old_new_root)
162+ # if, at this stage, root still has an old file_id, zap it so we can
163+ # stick a new one in.
164+ if (self.tree_file_id(self._new_root) is not None
165+ and self._new_root not in self._removed_id):
166+ self.unversion_file(self._new_root)
167+ if file_id is not None:
168+ self.version_file(self._new_root, file_id=file_id)
169+
170+ # Now move children of new root into old root directory.
171+ # Ensure all children are registered with the transaction, but don't
172+ # use directly-- some tree children have new parents
173+ list(self.iter_tree_children(old_new_root))
174+ # Move all children of new root into old root directory.
175+ for child in self.by_parent().get(old_new_root, []):
176+ self.adjust_path(self.final_name(child), self._new_root, child)
177+
178+ # Ensure old_new_root has no directory.
179+ if old_new_root in self._new_contents:
180+ self.cancel_creation(old_new_root)
181+ else:
182+ self.delete_contents(old_new_root)
183+
184+ # prevent deletion of root directory.
185+ if self._new_root in self._removed_contents:
186+ self.cancel_deletion(self._new_root)
187+
188+ # destroy path info for old_new_root.
189+ del self._new_parent[old_new_root]
190+ del self._new_name[old_new_root]
191+
192+ def trans_id_file_id(self, file_id):
193+ """Determine or set the transaction id associated with a file ID.
194+ A new id is only created for file_ids that were never present. If
195+ a transaction has been unversioned, it is deliberately still returned.
196+ (this will likely lead to an unversioned parent conflict.)
197+ """
198+ if file_id is None:
199+ raise ValueError('None is not a valid file id')
200+ if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
201+ return self._r_new_id[file_id]
202+ else:
203+ try:
204+ path = self._tree.id2path(file_id)
205+ except errors.NoSuchId:
206+ if file_id in self._non_present_ids:
207+ return self._non_present_ids[file_id]
208+ else:
209+ trans_id = self._assign_id()
210+ self._non_present_ids[file_id] = trans_id
211+ return trans_id
212+ else:
213+ return self.trans_id_tree_path(path)
214+
215+ def version_file(self, trans_id, file_id=None):
216+ """Schedule a file to become versioned."""
217+ raise NotImplementedError(self.version_file)
218+
219+ def cancel_versioning(self, trans_id):
220+ """Undo a previous versioning of a file"""
221+ raise NotImplementedError(self.cancel_versioning)
222+
223+ def new_paths(self, filesystem_only=False):
224+ """Determine the paths of all new and changed files.
225+
226+ :param filesystem_only: if True, only calculate values for files
227+ that require renames or execute bit changes.
228+ """
229+ new_ids = set()
230+ if filesystem_only:
231+ stale_ids = self._needs_rename.difference(self._new_name)
232+ stale_ids.difference_update(self._new_parent)
233+ stale_ids.difference_update(self._new_contents)
234+ stale_ids.difference_update(self._new_id)
235+ needs_rename = self._needs_rename.difference(stale_ids)
236+ id_sets = (needs_rename, self._new_executability)
237+ else:
238+ id_sets = (self._new_name, self._new_parent, self._new_contents,
239+ self._new_id, self._new_executability)
240+ for id_set in id_sets:
241+ new_ids.update(id_set)
242+ return sorted(FinalPaths(self).get_paths(new_ids))
243+
244+ def tree_file_id(self, trans_id):
245+ """Determine the file id associated with the trans_id in the tree"""
246+ path = self.tree_path(trans_id)
247+ if path is None:
248+ return None
249+ # the file is old; the old id is still valid
250+ if self._new_root == trans_id:
251+ return self._tree.path2id('')
252+ return self._tree.path2id(path)
253+
254+ def final_is_versioned(self, trans_id):
255+ return self.final_file_id(trans_id) is not None
256+
257+ def final_file_id(self, trans_id):
258+ """Determine the file id after any changes are applied, or None.
259+
260+ None indicates that the file will not be versioned after changes are
261+ applied.
262+ """
263+ try:
264+ return self._new_id[trans_id]
265+ except KeyError:
266+ if trans_id in self._removed_id:
267+ return None
268+ return self.tree_file_id(trans_id)
269+
270+ def inactive_file_id(self, trans_id):
271+ """Return the inactive file_id associated with a transaction id.
272+ That is, the one in the tree or in non_present_ids.
273+ The file_id may actually be active, too.
274+ """
275+ file_id = self.tree_file_id(trans_id)
276+ if file_id is not None:
277+ return file_id
278+ for key, value in viewitems(self._non_present_ids):
279+ if value == trans_id:
280+ return key
281+
282+ def find_conflicts(self):
283+ """Find any violations of inventory or filesystem invariants"""
284+ if self._done is True:
285+ raise ReusingTransform()
286+ conflicts = []
287+ # ensure all children of all existent parents are known
288+ # all children of non-existent parents are known, by definition.
289+ self._add_tree_children()
290+ by_parent = self.by_parent()
291+ conflicts.extend(self._unversioned_parents(by_parent))
292+ conflicts.extend(self._parent_loops())
293+ conflicts.extend(self._duplicate_entries(by_parent))
294+ conflicts.extend(self._parent_type_conflicts(by_parent))
295+ conflicts.extend(self._improper_versioning())
296+ conflicts.extend(self._executability_conflicts())
297+ conflicts.extend(self._overwrite_conflicts())
298+ return conflicts
299+
300+ def _check_malformed(self):
301+ conflicts = self.find_conflicts()
302+ if len(conflicts) != 0:
303+ raise MalformedTransform(conflicts=conflicts)
304+
305+ def _add_tree_children(self):
306+ """Add all the children of all active parents to the known paths.
307+
308+ Active parents are those which gain children, and those which are
309+ removed. This is a necessary first step in detecting conflicts.
310+ """
311+ parents = list(self.by_parent())
312+ parents.extend([t for t in self._removed_contents if
313+ self.tree_kind(t) == 'directory'])
314+ for trans_id in self._removed_id:
315+ path = self.tree_path(trans_id)
316+ if path is not None:
317+ if self._tree.stored_kind(path) == 'directory':
318+ parents.append(trans_id)
319+ elif self.tree_kind(trans_id) == 'directory':
320+ parents.append(trans_id)
321+
322+ for parent_id in parents:
323+ # ensure that all children are registered with the transaction
324+ list(self.iter_tree_children(parent_id))
325+
326+ def _has_named_child(self, name, parent_id, known_children):
327+ """Does a parent already have a name child.
328+
329+ :param name: The searched for name.
330+
331+ :param parent_id: The parent for which the check is made.
332+
333+ :param known_children: The already known children. This should have
334+ been recently obtained from `self.by_parent.get(parent_id)`
335+ (or will be if None is passed).
336+ """
337+ if known_children is None:
338+ known_children = self.by_parent().get(parent_id, [])
339+ for child in known_children:
340+ if self.final_name(child) == name:
341+ return True
342+ parent_path = self._tree_id_paths.get(parent_id, None)
343+ if parent_path is None:
344+ # No parent... no children
345+ return False
346+ child_path = joinpath(parent_path, name)
347+ child_id = self._tree_path_ids.get(child_path, None)
348+ if child_id is None:
349+ # Not known by the tree transform yet, check the filesystem
350+ return osutils.lexists(self._tree.abspath(child_path))
351+ else:
352+ raise AssertionError('child_id is missing: %s, %s, %s'
353+ % (name, parent_id, child_id))
354+
355+ def _available_backup_name(self, name, target_id):
356+ """Find an available backup name.
357+
358+ :param name: The basename of the file.
359+
360+ :param target_id: The directory trans_id where the backup should
361+ be placed.
362+ """
363+ known_children = self.by_parent().get(target_id, [])
364+ return osutils.available_backup_name(
365+ name,
366+ lambda base: self._has_named_child(
367+ base, target_id, known_children))
368+
369+ def _parent_loops(self):
370+ """No entry should be its own ancestor"""
371+ conflicts = []
372+ for trans_id in self._new_parent:
373+ seen = set()
374+ parent_id = trans_id
375+ while parent_id != ROOT_PARENT:
376+ seen.add(parent_id)
377+ try:
378+ parent_id = self.final_parent(parent_id)
379+ except KeyError:
380+ break
381+ if parent_id == trans_id:
382+ conflicts.append(('parent loop', trans_id))
383+ if parent_id in seen:
384+ break
385+ return conflicts
386+
387+ def _unversioned_parents(self, by_parent):
388+ """If parent directories are versioned, children must be versioned."""
389+ conflicts = []
390+ for parent_id, children in viewitems(by_parent):
391+ if parent_id == ROOT_PARENT:
392+ continue
393+ if self.final_is_versioned(parent_id):
394+ continue
395+ for child_id in children:
396+ if self.final_is_versioned(child_id):
397+ conflicts.append(('unversioned parent', parent_id))
398+ break
399+ return conflicts
400+
401+ def _improper_versioning(self):
402+ """Cannot version a file with no contents, or a bad type.
403+
404+ However, existing entries with no contents are okay.
405+ """
406+ conflicts = []
407+ for trans_id in self._new_id:
408+ kind = self.final_kind(trans_id)
409+ if kind == 'symlink' and not self._tree.supports_symlinks():
410+ # Ignore symlinks as they are not supported on this platform
411+ continue
412+ if kind is None:
413+ conflicts.append(('versioning no contents', trans_id))
414+ continue
415+ if not self._tree.versionable_kind(kind):
416+ conflicts.append(('versioning bad kind', trans_id, kind))
417+ return conflicts
418+
419+ def _executability_conflicts(self):
420+ """Check for bad executability changes.
421+
422+ Only versioned files may have their executability set, because
423+ 1. only versioned entries can have executability under windows
424+ 2. only files can be executable. (The execute bit on a directory
425+ does not indicate searchability)
426+ """
427+ conflicts = []
428+ for trans_id in self._new_executability:
429+ if not self.final_is_versioned(trans_id):
430+ conflicts.append(('unversioned executability', trans_id))
431+ else:
432+ if self.final_kind(trans_id) != "file":
433+ conflicts.append(('non-file executability', trans_id))
434+ return conflicts
435+
436+ def _overwrite_conflicts(self):
437+ """Check for overwrites (not permitted on Win32)"""
438+ conflicts = []
439+ for trans_id in self._new_contents:
440+ if self.tree_kind(trans_id) is None:
441+ continue
442+ if trans_id not in self._removed_contents:
443+ conflicts.append(('overwrite', trans_id,
444+ self.final_name(trans_id)))
445+ return conflicts
446+
447+ def _duplicate_entries(self, by_parent):
448+ """No directory may have two entries with the same name."""
449+ conflicts = []
450+ if (self._new_name, self._new_parent) == ({}, {}):
451+ return conflicts
452+ for children in viewvalues(by_parent):
453+ name_ids = []
454+ for child_tid in children:
455+ name = self.final_name(child_tid)
456+ if name is not None:
457+ # Keep children only if they still exist in the end
458+ if not self._case_sensitive_target:
459+ name = name.lower()
460+ name_ids.append((name, child_tid))
461+ name_ids.sort()
462+ last_name = None
463+ last_trans_id = None
464+ for name, trans_id in name_ids:
465+ kind = self.final_kind(trans_id)
466+ if kind is None and not self.final_is_versioned(trans_id):
467+ continue
468+ if name == last_name:
469+ conflicts.append(('duplicate', last_trans_id, trans_id,
470+ name))
471+ last_name = name
472+ last_trans_id = trans_id
473+ return conflicts
474+
475+ def _parent_type_conflicts(self, by_parent):
476+ """Children must have a directory parent"""
477+ conflicts = []
478+ for parent_id, children in viewitems(by_parent):
479+ if parent_id == ROOT_PARENT:
480+ continue
481+ no_children = True
482+ for child_id in children:
483+ if self.final_kind(child_id) is not None:
484+ no_children = False
485+ break
486+ if no_children:
487+ continue
488+ # There is at least a child, so we need an existing directory to
489+ # contain it.
490+ kind = self.final_kind(parent_id)
491+ if kind is None:
492+ # The directory will be deleted
493+ conflicts.append(('missing parent', parent_id))
494+ elif kind != "directory":
495+ # Meh, we need a *directory* to put something in it
496+ conflicts.append(('non-directory parent', parent_id))
497+ return conflicts
498+
499+ def _set_executability(self, path, trans_id):
500+ """Set the executability of versioned files """
501+ if self._tree._supports_executable():
502+ new_executability = self._new_executability[trans_id]
503+ abspath = self._tree.abspath(path)
504+ current_mode = os.stat(abspath).st_mode
505+ if new_executability:
506+ umask = os.umask(0)
507+ os.umask(umask)
508+ to_mode = current_mode | (0o100 & ~umask)
509+ # Enable x-bit for others only if they can read it.
510+ if current_mode & 0o004:
511+ to_mode |= 0o001 & ~umask
512+ if current_mode & 0o040:
513+ to_mode |= 0o010 & ~umask
514+ else:
515+ to_mode = current_mode & ~0o111
516+ osutils.chmod_if_possible(abspath, to_mode)
517+
518+ def _new_entry(self, name, parent_id, file_id):
519+ """Helper function to create a new filesystem entry."""
520+ trans_id = self.create_path(name, parent_id)
521+ if file_id is not None:
522+ self.version_file(trans_id, file_id=file_id)
523+ return trans_id
524+
525+ def new_file(self, name, parent_id, contents, file_id=None,
526+ executable=None, sha1=None):
527+ """Convenience method to create files.
528+
529+ name is the name of the file to create.
530+ parent_id is the transaction id of the parent directory of the file.
531+ contents is an iterator of bytestrings, which will be used to produce
532+ the file.
533+ :param file_id: The inventory ID of the file, if it is to be versioned.
534+ :param executable: Only valid when a file_id has been supplied.
535+ """
536+ trans_id = self._new_entry(name, parent_id, file_id)
537+ # TODO: rather than scheduling a set_executable call,
538+ # have create_file create the file with the right mode.
539+ self.create_file(contents, trans_id, sha1=sha1)
540+ if executable is not None:
541+ self.set_executability(executable, trans_id)
542+ return trans_id
543+
544+ def new_directory(self, name, parent_id, file_id=None):
545+ """Convenience method to create directories.
546+
547+ name is the name of the directory to create.
548+ parent_id is the transaction id of the parent directory of the
549+ directory.
550+ file_id is the inventory ID of the directory, if it is to be versioned.
551+ """
552+ trans_id = self._new_entry(name, parent_id, file_id)
553+ self.create_directory(trans_id)
554+ return trans_id
555+
556+ def new_symlink(self, name, parent_id, target, file_id=None):
557+ """Convenience method to create symbolic link.
558+
559+ name is the name of the symlink to create.
560+ parent_id is the transaction id of the parent directory of the symlink.
561+ target is a bytestring of the target of the symlink.
562+ file_id is the inventory ID of the file, if it is to be versioned.
563+ """
564+ trans_id = self._new_entry(name, parent_id, file_id)
565+ self.create_symlink(target, trans_id)
566+ return trans_id
567+
568+ def new_orphan(self, trans_id, parent_id):
569+ """Schedule an item to be orphaned.
570+
571+ When a directory is about to be removed, its children, if they are not
572+ versioned are moved out of the way: they don't have a parent anymore.
573+
574+ :param trans_id: The trans_id of the existing item.
575+ :param parent_id: The parent trans_id of the item.
576+ """
577+ raise NotImplementedError(self.new_orphan)
578+
579+ def _get_potential_orphans(self, dir_id):
580+ """Find the potential orphans in a directory.
581+
582+ A directory can't be safely deleted if there are versioned files in it.
583+ If all the contained files are unversioned then they can be orphaned.
584+
585+ The 'None' return value means that the directory contains at least one
586+ versioned file and should not be deleted.
587+
588+ :param dir_id: The directory trans id.
589+
590+ :return: A list of the orphan trans ids or None if at least one
591+ versioned file is present.
592+ """
593+ orphans = []
594+ # Find the potential orphans, stop if one item should be kept
595+ for child_tid in self.by_parent()[dir_id]:
596+ if child_tid in self._removed_contents:
597+ # The child is removed as part of the transform. Since it was
598+ # versioned before, it's not an orphan
599+ continue
600+ if not self.final_is_versioned(child_tid):
601+ # The child is not versioned
602+ orphans.append(child_tid)
603+ else:
604+ # We have a versioned file here, searching for orphans is
605+ # meaningless.
606+ orphans = None
607+ break
608+ return orphans
609+
610+ def _affected_ids(self):
611+ """Return the set of transform ids affected by the transform"""
612+ trans_ids = set(self._removed_id)
613+ trans_ids.update(self._new_id)
614+ trans_ids.update(self._removed_contents)
615+ trans_ids.update(self._new_contents)
616+ trans_ids.update(self._new_executability)
617+ trans_ids.update(self._new_name)
618+ trans_ids.update(self._new_parent)
619+ return trans_ids
620+
621+ def _get_file_id_maps(self):
622+ """Return mapping of file_ids to trans_ids in the to and from states"""
623+ trans_ids = self._affected_ids()
624+ from_trans_ids = {}
625+ to_trans_ids = {}
626+ # Build up two dicts: trans_ids associated with file ids in the
627+ # FROM state, vs the TO state.
628+ for trans_id in trans_ids:
629+ from_file_id = self.tree_file_id(trans_id)
630+ if from_file_id is not None:
631+ from_trans_ids[from_file_id] = trans_id
632+ to_file_id = self.final_file_id(trans_id)
633+ if to_file_id is not None:
634+ to_trans_ids[to_file_id] = trans_id
635+ return from_trans_ids, to_trans_ids
636+
637+ def _from_file_data(self, from_trans_id, from_versioned, from_path):
638+ """Get data about a file in the from (tree) state
639+
640+ Return a (name, parent, kind, executable) tuple
641+ """
642+ from_path = self._tree_id_paths.get(from_trans_id)
643+ if from_versioned:
644+ # get data from working tree if versioned
645+ from_entry = next(self._tree.iter_entries_by_dir(
646+ specific_files=[from_path]))[1]
647+ from_name = from_entry.name
648+ from_parent = from_entry.parent_id
649+ else:
650+ from_entry = None
651+ if from_path is None:
652+ # File does not exist in FROM state
653+ from_name = None
654+ from_parent = None
655+ else:
656+ # File exists, but is not versioned. Have to use path-
657+ # splitting stuff
658+ from_name = os.path.basename(from_path)
659+ tree_parent = self.get_tree_parent(from_trans_id)
660+ from_parent = self.tree_file_id(tree_parent)
661+ if from_path is not None:
662+ from_kind, from_executable, from_stats = \
663+ self._tree._comparison_data(from_entry, from_path)
664+ else:
665+ from_kind = None
666+ from_executable = False
667+ return from_name, from_parent, from_kind, from_executable
668+
669+ def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
670+ """Get data about a file in the to (target) state
671+
672+ Return a (name, parent, kind, executable) tuple
673+ """
674+ to_name = self.final_name(to_trans_id)
675+ to_kind = self.final_kind(to_trans_id)
676+ to_parent = self.final_file_id(self.final_parent(to_trans_id))
677+ if to_trans_id in self._new_executability:
678+ to_executable = self._new_executability[to_trans_id]
679+ elif to_trans_id == from_trans_id:
680+ to_executable = from_executable
681+ else:
682+ to_executable = False
683+ return to_name, to_parent, to_kind, to_executable
684+
685+ def iter_changes(self):
686+ """Produce output in the same format as Tree.iter_changes.
687+
688+ Will produce nonsensical results if invoked while inventory/filesystem
689+ conflicts (as reported by TreeTransform.find_conflicts()) are present.
690+
691+ This reads the Transform, but only reproduces changes involving a
692+ file_id. Files that are not versioned in either of the FROM or TO
693+ states are not reflected.
694+ """
695+ final_paths = FinalPaths(self)
696+ from_trans_ids, to_trans_ids = self._get_file_id_maps()
697+ results = []
698+ # Now iterate through all active file_ids
699+ for file_id in set(from_trans_ids).union(to_trans_ids):
700+ modified = False
701+ from_trans_id = from_trans_ids.get(file_id)
702+ # find file ids, and determine versioning state
703+ if from_trans_id is None:
704+ from_versioned = False
705+ from_trans_id = to_trans_ids[file_id]
706+ else:
707+ from_versioned = True
708+ to_trans_id = to_trans_ids.get(file_id)
709+ if to_trans_id is None:
710+ to_versioned = False
711+ to_trans_id = from_trans_id
712+ else:
713+ to_versioned = True
714+
715+ if not from_versioned:
716+ from_path = None
717+ else:
718+ from_path = self._tree_id_paths.get(from_trans_id)
719+ if not to_versioned:
720+ to_path = None
721+ else:
722+ to_path = final_paths.get_path(to_trans_id)
723+
724+ from_name, from_parent, from_kind, from_executable = \
725+ self._from_file_data(from_trans_id, from_versioned, from_path)
726+
727+ to_name, to_parent, to_kind, to_executable = \
728+ self._to_file_data(to_trans_id, from_trans_id, from_executable)
729+
730+ if from_kind != to_kind:
731+ modified = True
732+ elif to_kind in ('file', 'symlink') and (
733+ to_trans_id != from_trans_id
734+ or to_trans_id in self._new_contents):
735+ modified = True
736+ if (not modified and from_versioned == to_versioned
737+ and from_parent == to_parent and from_name == to_name
738+ and from_executable == to_executable):
739+ continue
740+ results.append(
741+ TreeChange(
742+ file_id, (from_path, to_path), modified,
743+ (from_versioned, to_versioned),
744+ (from_parent, to_parent),
745+ (from_name, to_name),
746+ (from_kind, to_kind),
747+ (from_executable, to_executable)))
748+
749+ def path_key(c):
750+ return (c.path[0] or '', c.path[1] or '')
751+ return iter(sorted(results, key=path_key))
752+
753+ def get_preview_tree(self):
754+ """Return a tree representing the result of the transform.
755+
756+ The tree is a snapshot, and altering the TreeTransform will invalidate
757+ it.
758+ """
759+ raise NotImplementedError(self.get_preview)
760+
761+ def commit(self, branch, message, merge_parents=None, strict=False,
762+ timestamp=None, timezone=None, committer=None, authors=None,
763+ revprops=None, revision_id=None):
764+ """Commit the result of this TreeTransform to a branch.
765+
766+ :param branch: The branch to commit to.
767+ :param message: The message to attach to the commit.
768+ :param merge_parents: Additional parent revision-ids specified by
769+ pending merges.
770+ :param strict: If True, abort the commit if there are unversioned
771+ files.
772+ :param timestamp: if not None, seconds-since-epoch for the time and
773+ date. (May be a float.)
774+ :param timezone: Optional timezone for timestamp, as an offset in
775+ seconds.
776+ :param committer: Optional committer in email-id format.
777+ (e.g. "J Random Hacker <jrandom@example.com>")
778+ :param authors: Optional list of authors in email-id format.
779+ :param revprops: Optional dictionary of revision properties.
780+ :param revision_id: Optional revision id. (Specifying a revision-id
781+ may reduce performance for some non-native formats.)
782+ :return: The revision_id of the revision committed.
783+ """
784+ self._check_malformed()
785+ if strict:
786+ unversioned = set(self._new_contents).difference(set(self._new_id))
787+ for trans_id in unversioned:
788+ if not self.final_is_versioned(trans_id):
789+ raise errors.StrictCommitFailed()
790+
791+ revno, last_rev_id = branch.last_revision_info()
792+ if last_rev_id == _mod_revision.NULL_REVISION:
793+ if merge_parents is not None:
794+ raise ValueError('Cannot supply merge parents for first'
795+ ' commit.')
796+ parent_ids = []
797+ else:
798+ parent_ids = [last_rev_id]
799+ if merge_parents is not None:
800+ parent_ids.extend(merge_parents)
801+ if self._tree.get_revision_id() != last_rev_id:
802+ raise ValueError('TreeTransform not based on branch basis: %s' %
803+ self._tree.get_revision_id().decode('utf-8'))
804+ from .. import commit
805+ revprops = commit.Commit.update_revprops(revprops, branch, authors)
806+ builder = branch.get_commit_builder(parent_ids,
807+ timestamp=timestamp,
808+ timezone=timezone,
809+ committer=committer,
810+ revprops=revprops,
811+ revision_id=revision_id)
812+ preview = self.get_preview_tree()
813+ list(builder.record_iter_changes(preview, last_rev_id,
814+ self.iter_changes()))
815+ builder.finish_inventory()
816+ revision_id = builder.commit(message)
817+ branch.set_last_revision_info(revno + 1, revision_id)
818+ return revision_id
819+
820+ def _text_parent(self, trans_id):
821+ path = self.tree_path(trans_id)
822+ try:
823+ if path is None or self._tree.kind(path) != 'file':
824+ return None
825+ except errors.NoSuchFile:
826+ return None
827+ return path
828+
829+ def _get_parents_texts(self, trans_id):
830+ """Get texts for compression parents of this file."""
831+ path = self._text_parent(trans_id)
832+ if path is None:
833+ return ()
834+ return (self._tree.get_file_text(path),)
835+
836+ def _get_parents_lines(self, trans_id):
837+ """Get lines for compression parents of this file."""
838+ path = self._text_parent(trans_id)
839+ if path is None:
840+ return ()
841+ return (self._tree.get_file_lines(path),)
842+
843+ def serialize(self, serializer):
844+ """Serialize this TreeTransform.
845+
846+ :param serializer: A Serialiser like pack.ContainerSerializer.
847+ """
848+ from .. import bencode
849+ new_name = {k.encode('utf-8'): v.encode('utf-8')
850+ for k, v in viewitems(self._new_name)}
851+ new_parent = {k.encode('utf-8'): v.encode('utf-8')
852+ for k, v in viewitems(self._new_parent)}
853+ new_id = {k.encode('utf-8'): v
854+ for k, v in viewitems(self._new_id)}
855+ new_executability = {k.encode('utf-8'): int(v)
856+ for k, v in viewitems(self._new_executability)}
857+ tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
858+ for k, v in viewitems(self._tree_path_ids)}
859+ non_present_ids = {k: v.encode('utf-8')
860+ for k, v in viewitems(self._non_present_ids)}
861+ removed_contents = [trans_id.encode('utf-8')
862+ for trans_id in self._removed_contents]
863+ removed_id = [trans_id.encode('utf-8')
864+ for trans_id in self._removed_id]
865+ attribs = {
866+ b'_id_number': self._id_number,
867+ b'_new_name': new_name,
868+ b'_new_parent': new_parent,
869+ b'_new_executability': new_executability,
870+ b'_new_id': new_id,
871+ b'_tree_path_ids': tree_path_ids,
872+ b'_removed_id': removed_id,
873+ b'_removed_contents': removed_contents,
874+ b'_non_present_ids': non_present_ids,
875+ }
876+ yield serializer.bytes_record(bencode.bencode(attribs),
877+ ((b'attribs',),))
878+ for trans_id, kind in sorted(viewitems(self._new_contents)):
879+ if kind == 'file':
880+ with open(self._limbo_name(trans_id), 'rb') as cur_file:
881+ lines = cur_file.readlines()
882+ parents = self._get_parents_lines(trans_id)
883+ mpdiff = multiparent.MultiParent.from_lines(lines, parents)
884+ content = b''.join(mpdiff.to_patch())
885+ if kind == 'directory':
886+ content = b''
887+ if kind == 'symlink':
888+ content = self._read_symlink_target(trans_id)
889+ if not isinstance(content, bytes):
890+ content = content.encode('utf-8')
891+ yield serializer.bytes_record(
892+ content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
893+
894+ def deserialize(self, records):
895+ """Deserialize a stored TreeTransform.
896+
897+ :param records: An iterable of (names, content) tuples, as per
898+ pack.ContainerPushParser.
899+ """
900+ from .. import bencode
901+ names, content = next(records)
902+ attribs = bencode.bdecode(content)
903+ self._id_number = attribs[b'_id_number']
904+ self._new_name = {k.decode('utf-8'): v.decode('utf-8')
905+ for k, v in viewitems(attribs[b'_new_name'])}
906+ self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
907+ for k, v in viewitems(attribs[b'_new_parent'])}
908+ self._new_executability = {
909+ k.decode('utf-8'): bool(v)
910+ for k, v in viewitems(attribs[b'_new_executability'])}
911+ self._new_id = {k.decode('utf-8'): v
912+ for k, v in viewitems(attribs[b'_new_id'])}
913+ self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
914+ self._tree_path_ids = {}
915+ self._tree_id_paths = {}
916+ for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
917+ path = bytepath.decode('utf-8')
918+ trans_id = trans_id.decode('utf-8')
919+ self._tree_path_ids[path] = trans_id
920+ self._tree_id_paths[trans_id] = path
921+ self._removed_id = {trans_id.decode('utf-8')
922+ for trans_id in attribs[b'_removed_id']}
923+ self._removed_contents = set(
924+ trans_id.decode('utf-8')
925+ for trans_id in attribs[b'_removed_contents'])
926+ self._non_present_ids = {
927+ k: v.decode('utf-8')
928+ for k, v in viewitems(attribs[b'_non_present_ids'])}
929+ for ((trans_id, kind),), content in records:
930+ trans_id = trans_id.decode('utf-8')
931+ kind = kind.decode('ascii')
932+ if kind == 'file':
933+ mpdiff = multiparent.MultiParent.from_patch(content)
934+ lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
935+ self.create_file(lines, trans_id)
936+ if kind == 'directory':
937+ self.create_directory(trans_id)
938+ if kind == 'symlink':
939+ self.create_symlink(content.decode('utf-8'), trans_id)
940+
941+ def create_file(self, contents, trans_id, mode_id=None, sha1=None):
942+ """Schedule creation of a new file.
943+
944+ :seealso: new_file.
945+
946+ :param contents: an iterator of strings, all of which will be written
947+ to the target destination.
948+ :param trans_id: TreeTransform handle
949+ :param mode_id: If not None, force the mode of the target file to match
950+ the mode of the object referenced by mode_id.
951+ Otherwise, we will try to preserve mode bits of an existing file.
952+ :param sha1: If the sha1 of this content is already known, pass it in.
953+ We can use it to prevent future sha1 computations.
954+ """
955+ raise NotImplementedError(self.create_file)
956+
957+ def create_directory(self, trans_id):
958+ """Schedule creation of a new directory.
959+
960+ See also new_directory.
961+ """
962+ raise NotImplementedError(self.create_directory)
963+
964+ def create_symlink(self, target, trans_id):
965+ """Schedule creation of a new symbolic link.
966+
967+ target is a bytestring.
968+ See also new_symlink.
969+ """
970+ raise NotImplementedError(self.create_symlink)
971+
972+ def create_hardlink(self, path, trans_id):
973+ """Schedule creation of a hard link"""
974+ raise NotImplementedError(self.create_hardlink)
975+
976+ def cancel_creation(self, trans_id):
977+ """Cancel the creation of new file contents."""
978+ raise NotImplementedError(self.cancel_creation)
979+
980+ def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
981+ """Apply all changes to the inventory and filesystem.
982+
983+ If filesystem or inventory conflicts are present, MalformedTransform
984+ will be thrown.
985+
986+ If apply succeeds, finalize is not necessary.
987+
988+ :param no_conflicts: if True, the caller guarantees there are no
989+ conflicts, so no check is made.
990+ :param precomputed_delta: An inventory delta to use instead of
991+ calculating one.
992+ :param _mover: Supply an alternate FileMover, for testing
993+ """
994+ raise NotImplementedError(self.apply)
995+
996+
997+class DiskTreeTransform(TreeTransformBase):
998+ """Tree transform storing its contents on disk."""
999+
1000+ def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1001+ """Constructor.
1002+ :param tree: The tree that will be transformed, but not necessarily
1003+ the output tree.
1004+ :param limbodir: A directory where new files can be stored until
1005+ they are installed in their proper places
1006+ :param pb: ignored
1007+ :param case_sensitive: If True, the target of the transform is
1008+ case sensitive, not just case preserving.
1009+ """
1010+ TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1011+ self._limbodir = limbodir
1012+ self._deletiondir = None
1013+ # A mapping of transform ids to their limbo filename
1014+ self._limbo_files = {}
1015+ self._possibly_stale_limbo_files = set()
1016+ # A mapping of transform ids to a set of the transform ids of children
1017+ # that their limbo directory has
1018+ self._limbo_children = {}
1019+ # Map transform ids to maps of child filename to child transform id
1020+ self._limbo_children_names = {}
1021+ # List of transform ids that need to be renamed from limbo into place
1022+ self._needs_rename = set()
1023+ self._creation_mtime = None
1024+ self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1025+
1026+ def finalize(self):
1027+ """Release the working tree lock, if held, clean up limbo dir.
1028+
1029+ This is required if apply has not been invoked, but can be invoked
1030+ even after apply.
1031+ """
1032+ if self._tree is None:
1033+ return
1034+ try:
1035+ limbo_paths = list(viewvalues(self._limbo_files))
1036+ limbo_paths.extend(self._possibly_stale_limbo_files)
1037+ limbo_paths.sort(reverse=True)
1038+ for path in limbo_paths:
1039+ try:
1040+ osutils.delete_any(path)
1041+ except OSError as e:
1042+ if e.errno != errno.ENOENT:
1043+ raise
1044+ # XXX: warn? perhaps we just got interrupted at an
1045+ # inconvenient moment, but perhaps files are disappearing
1046+ # from under us?
1047+ try:
1048+ osutils.delete_any(self._limbodir)
1049+ except OSError:
1050+ # We don't especially care *why* the dir is immortal.
1051+ raise ImmortalLimbo(self._limbodir)
1052+ try:
1053+ if self._deletiondir is not None:
1054+ osutils.delete_any(self._deletiondir)
1055+ except OSError:
1056+ raise errors.ImmortalPendingDeletion(self._deletiondir)
1057+ finally:
1058+ TreeTransformBase.finalize(self)
1059+
1060+ def _limbo_supports_executable(self):
1061+ """Check if the limbo path supports the executable bit."""
1062+ return osutils.supports_executable(self._limbodir)
1063+
1064+ def _limbo_name(self, trans_id):
1065+ """Generate the limbo name of a file"""
1066+ limbo_name = self._limbo_files.get(trans_id)
1067+ if limbo_name is None:
1068+ limbo_name = self._generate_limbo_path(trans_id)
1069+ self._limbo_files[trans_id] = limbo_name
1070+ return limbo_name
1071+
1072+ def _generate_limbo_path(self, trans_id):
1073+ """Generate a limbo path using the trans_id as the relative path.
1074+
1075+ This is suitable as a fallback, and when the transform should not be
1076+ sensitive to the path encoding of the limbo directory.
1077+ """
1078+ self._needs_rename.add(trans_id)
1079+ return osutils.pathjoin(self._limbodir, trans_id)
1080+
1081+ def adjust_path(self, name, parent, trans_id):
1082+ previous_parent = self._new_parent.get(trans_id)
1083+ previous_name = self._new_name.get(trans_id)
1084+ super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
1085+ if (trans_id in self._limbo_files
1086+ and trans_id not in self._needs_rename):
1087+ self._rename_in_limbo([trans_id])
1088+ if previous_parent != parent:
1089+ self._limbo_children[previous_parent].remove(trans_id)
1090+ if previous_parent != parent or previous_name != name:
1091+ del self._limbo_children_names[previous_parent][previous_name]
1092+
1093+ def _rename_in_limbo(self, trans_ids):
1094+ """Fix limbo names so that the right final path is produced.
1095+
1096+ This means we outsmarted ourselves-- we tried to avoid renaming
1097+ these files later by creating them with their final names in their
1098+ final parents. But now the previous name or parent is no longer
1099+ suitable, so we have to rename them.
1100+
1101+ Even for trans_ids that have no new contents, we must remove their
1102+ entries from _limbo_files, because they are now stale.
1103+ """
1104+ for trans_id in trans_ids:
1105+ old_path = self._limbo_files[trans_id]
1106+ self._possibly_stale_limbo_files.add(old_path)
1107+ del self._limbo_files[trans_id]
1108+ if trans_id not in self._new_contents:
1109+ continue
1110+ new_path = self._limbo_name(trans_id)
1111+ os.rename(old_path, new_path)
1112+ self._possibly_stale_limbo_files.remove(old_path)
1113+ for descendant in self._limbo_descendants(trans_id):
1114+ desc_path = self._limbo_files[descendant]
1115+ desc_path = new_path + desc_path[len(old_path):]
1116+ self._limbo_files[descendant] = desc_path
1117+
1118+ def _limbo_descendants(self, trans_id):
1119+ """Return the set of trans_ids whose limbo paths descend from this."""
1120+ descendants = set(self._limbo_children.get(trans_id, []))
1121+ for descendant in list(descendants):
1122+ descendants.update(self._limbo_descendants(descendant))
1123+ return descendants
1124+
1125+ def _set_mode(self, trans_id, mode_id, typefunc):
1126+ raise NotImplementedError(self._set_mode)
1127+
1128+ def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1129+ """Schedule creation of a new file.
1130+
1131+ :seealso: new_file.
1132+
1133+ :param contents: an iterator of strings, all of which will be written
1134+ to the target destination.
1135+ :param trans_id: TreeTransform handle
1136+ :param mode_id: If not None, force the mode of the target file to match
1137+ the mode of the object referenced by mode_id.
1138+ Otherwise, we will try to preserve mode bits of an existing file.
1139+ :param sha1: If the sha1 of this content is already known, pass it in.
1140+ We can use it to prevent future sha1 computations.
1141+ """
1142+ name = self._limbo_name(trans_id)
1143+ with open(name, 'wb') as f:
1144+ unique_add(self._new_contents, trans_id, 'file')
1145+ f.writelines(contents)
1146+ self._set_mtime(name)
1147+ self._set_mode(trans_id, mode_id, S_ISREG)
1148+ # It is unfortunate we have to use lstat instead of fstat, but we just
1149+ # used utime and chmod on the file, so we need the accurate final
1150+ # details.
1151+ if sha1 is not None:
1152+ self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1153+
1154+ def _read_symlink_target(self, trans_id):
1155+ return os.readlink(self._limbo_name(trans_id))
1156+
1157+ def _set_mtime(self, path):
1158+ """All files that are created get the same mtime.
1159+
1160+ This time is set by the first object to be created.
1161+ """
1162+ if self._creation_mtime is None:
1163+ self._creation_mtime = time.time()
1164+ os.utime(path, (self._creation_mtime, self._creation_mtime))
1165+
1166+ def create_hardlink(self, path, trans_id):
1167+ """Schedule creation of a hard link"""
1168+ name = self._limbo_name(trans_id)
1169+ try:
1170+ os.link(path, name)
1171+ except OSError as e:
1172+ if e.errno != errno.EPERM:
1173+ raise
1174+ raise errors.HardLinkNotSupported(path)
1175+ try:
1176+ unique_add(self._new_contents, trans_id, 'file')
1177+ except BaseException:
1178+ # Clean up the file, it never got registered so
1179+ # TreeTransform.finalize() won't clean it up.
1180+ os.unlink(name)
1181+ raise
1182+
1183+ def create_directory(self, trans_id):
1184+ """Schedule creation of a new directory.
1185+
1186+ See also new_directory.
1187+ """
1188+ os.mkdir(self._limbo_name(trans_id))
1189+ unique_add(self._new_contents, trans_id, 'directory')
1190+
1191+ def create_symlink(self, target, trans_id):
1192+ """Schedule creation of a new symbolic link.
1193+
1194+ target is a bytestring.
1195+ See also new_symlink.
1196+ """
1197+ if self._create_symlinks:
1198+ os.symlink(target, self._limbo_name(trans_id))
1199+ else:
1200+ try:
1201+ path = FinalPaths(self).get_path(trans_id)
1202+ except KeyError:
1203+ path = None
1204+ trace.warning(
1205+ 'Unable to create symlink "%s" on this filesystem.' % (path,))
1206+ # We add symlink to _new_contents even if they are unsupported
1207+ # and not created. These entries are subsequently used to avoid
1208+ # conflicts on platforms that don't support symlink
1209+ unique_add(self._new_contents, trans_id, 'symlink')
1210+
1211+ def cancel_creation(self, trans_id):
1212+ """Cancel the creation of new file contents."""
1213+ del self._new_contents[trans_id]
1214+ if trans_id in self._observed_sha1s:
1215+ del self._observed_sha1s[trans_id]
1216+ children = self._limbo_children.get(trans_id)
1217+ # if this is a limbo directory with children, move them before removing
1218+ # the directory
1219+ if children is not None:
1220+ self._rename_in_limbo(children)
1221+ del self._limbo_children[trans_id]
1222+ del self._limbo_children_names[trans_id]
1223+ osutils.delete_any(self._limbo_name(trans_id))
1224+
1225+ def new_orphan(self, trans_id, parent_id):
1226+ conf = self._tree.get_config_stack()
1227+ handle_orphan = conf.get('transform.orphan_policy')
1228+ handle_orphan(self, trans_id, parent_id)
1229+
1230+
1231 class InventoryTreeTransform(DiskTreeTransform):
1232 """Represent a tree transformation.
1233
1234
1235=== modified file 'breezy/git/transform.py'
1236--- breezy/git/transform.py 2020-07-05 19:02:14 +0000
1237+++ breezy/git/transform.py 2020-07-06 02:29:08 +0000
1238@@ -19,25 +19,1217 @@
1239
1240 import errno
1241 import os
1242+from stat import S_ISREG
1243+import time
1244
1245-from .. import errors, osutils, ui, urlutils
1246+from .. import errors, multiparent, osutils, trace, ui, urlutils
1247 from ..i18n import gettext
1248 from ..mutabletree import MutableTree
1249-from ..sixish import viewitems
1250+from ..sixish import viewitems, viewvalues
1251 from ..transform import (
1252- DiskTreeTransform,
1253+ TreeTransform,
1254 _TransformResults,
1255 _FileMover,
1256 FinalPaths,
1257 joinpath,
1258 unique_add,
1259 TransformRenameFailed,
1260+ ImmortalLimbo,
1261+ ROOT_PARENT,
1262+ ReusingTransform,
1263+ MalformedTransform,
1264 )
1265
1266 from ..bzr import inventory
1267 from ..bzr.transform import TransformPreview as GitTransformPreview
1268
1269
1270+class TreeTransformBase(TreeTransform):
1271+ """The base class for TreeTransform and its kin."""
1272+
1273+ def __init__(self, tree, pb=None, case_sensitive=True):
1274+ """Constructor.
1275+
1276+ :param tree: The tree that will be transformed, but not necessarily
1277+ the output tree.
1278+ :param pb: ignored
1279+ :param case_sensitive: If True, the target of the transform is
1280+ case sensitive, not just case preserving.
1281+ """
1282+ super(TreeTransformBase, self).__init__(tree, pb=pb)
1283+ # mapping of trans_id => (sha1 of content, stat_value)
1284+ self._observed_sha1s = {}
1285+ # Mapping of trans_id -> new file_id
1286+ self._new_id = {}
1287+ # Mapping of old file-id -> trans_id
1288+ self._non_present_ids = {}
1289+ # Mapping of new file_id -> trans_id
1290+ self._r_new_id = {}
1291+ # The trans_id that will be used as the tree root
1292+ if tree.is_versioned(''):
1293+ self._new_root = self.trans_id_tree_path('')
1294+ else:
1295+ self._new_root = None
1296+ # Whether the target is case sensitive
1297+ self._case_sensitive_target = case_sensitive
1298+
1299+ def finalize(self):
1300+ """Release the working tree lock, if held.
1301+
1302+ This is required if apply has not been invoked, but can be invoked
1303+ even after apply.
1304+ """
1305+ if self._tree is None:
1306+ return
1307+ for hook in MutableTree.hooks['post_transform']:
1308+ hook(self._tree, self)
1309+ self._tree.unlock()
1310+ self._tree = None
1311+
1312+ def __get_root(self):
1313+ return self._new_root
1314+
1315+ root = property(__get_root)
1316+
1317+ def create_path(self, name, parent):
1318+ """Assign a transaction id to a new path"""
1319+ trans_id = self._assign_id()
1320+ unique_add(self._new_name, trans_id, name)
1321+ unique_add(self._new_parent, trans_id, parent)
1322+ return trans_id
1323+
1324+ def adjust_root_path(self, name, parent):
1325+ """Emulate moving the root by moving all children, instead.
1326+
1327+ We do this by undoing the association of root's transaction id with the
1328+ current tree. This allows us to create a new directory with that
1329+ transaction id. We unversion the root directory and version the
1330+ physically new directory, and hope someone versions the tree root
1331+ later.
1332+ """
1333+ old_root = self._new_root
1334+ old_root_file_id = self.final_file_id(old_root)
1335+ # force moving all children of root
1336+ for child_id in self.iter_tree_children(old_root):
1337+ if child_id != parent:
1338+ self.adjust_path(self.final_name(child_id),
1339+ self.final_parent(child_id), child_id)
1340+ file_id = self.final_file_id(child_id)
1341+ if file_id is not None:
1342+ self.unversion_file(child_id)
1343+ self.version_file(child_id, file_id=file_id)
1344+
1345+ # the physical root needs a new transaction id
1346+ self._tree_path_ids.pop("")
1347+ self._tree_id_paths.pop(old_root)
1348+ self._new_root = self.trans_id_tree_path('')
1349+ if parent == old_root:
1350+ parent = self._new_root
1351+ self.adjust_path(name, parent, old_root)
1352+ self.create_directory(old_root)
1353+ self.version_file(old_root, file_id=old_root_file_id)
1354+ self.unversion_file(self._new_root)
1355+
1356+ def fixup_new_roots(self):
1357+ """Reinterpret requests to change the root directory
1358+
1359+ Instead of creating a root directory, or moving an existing directory,
1360+ all the attributes and children of the new root are applied to the
1361+ existing root directory.
1362+
1363+ This means that the old root trans-id becomes obsolete, so it is
1364+ recommended only to invoke this after the root trans-id has become
1365+ irrelevant.
1366+
1367+ """
1368+ new_roots = [k for k, v in viewitems(self._new_parent)
1369+ if v == ROOT_PARENT]
1370+ if len(new_roots) < 1:
1371+ return
1372+ if len(new_roots) != 1:
1373+ raise ValueError('A tree cannot have two roots!')
1374+ if self._new_root is None:
1375+ self._new_root = new_roots[0]
1376+ return
1377+ old_new_root = new_roots[0]
1378+ # unversion the new root's directory.
1379+ if self.final_kind(self._new_root) is None:
1380+ file_id = self.final_file_id(old_new_root)
1381+ else:
1382+ file_id = self.final_file_id(self._new_root)
1383+ if old_new_root in self._new_id:
1384+ self.cancel_versioning(old_new_root)
1385+ else:
1386+ self.unversion_file(old_new_root)
1387+ # if, at this stage, root still has an old file_id, zap it so we can
1388+ # stick a new one in.
1389+ if (self.tree_file_id(self._new_root) is not None
1390+ and self._new_root not in self._removed_id):
1391+ self.unversion_file(self._new_root)
1392+ if file_id is not None:
1393+ self.version_file(self._new_root, file_id=file_id)
1394+
1395+ # Now move children of new root into old root directory.
1396+ # Ensure all children are registered with the transaction, but don't
1397+ # use directly-- some tree children have new parents
1398+ list(self.iter_tree_children(old_new_root))
1399+ # Move all children of new root into old root directory.
1400+ for child in self.by_parent().get(old_new_root, []):
1401+ self.adjust_path(self.final_name(child), self._new_root, child)
1402+
1403+ # Ensure old_new_root has no directory.
1404+ if old_new_root in self._new_contents:
1405+ self.cancel_creation(old_new_root)
1406+ else:
1407+ self.delete_contents(old_new_root)
1408+
1409+ # prevent deletion of root directory.
1410+ if self._new_root in self._removed_contents:
1411+ self.cancel_deletion(self._new_root)
1412+
1413+ # destroy path info for old_new_root.
1414+ del self._new_parent[old_new_root]
1415+ del self._new_name[old_new_root]
1416+
1417+ def trans_id_file_id(self, file_id):
1418+ """Determine or set the transaction id associated with a file ID.
1419+ A new id is only created for file_ids that were never present. If
1420+ a transaction has been unversioned, it is deliberately still returned.
1421+ (this will likely lead to an unversioned parent conflict.)
1422+ """
1423+ if file_id is None:
1424+ raise ValueError('None is not a valid file id')
1425+ if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
1426+ return self._r_new_id[file_id]
1427+ else:
1428+ try:
1429+ path = self._tree.id2path(file_id)
1430+ except errors.NoSuchId:
1431+ if file_id in self._non_present_ids:
1432+ return self._non_present_ids[file_id]
1433+ else:
1434+ trans_id = self._assign_id()
1435+ self._non_present_ids[file_id] = trans_id
1436+ return trans_id
1437+ else:
1438+ return self.trans_id_tree_path(path)
1439+
1440+ def version_file(self, trans_id, file_id=None):
1441+ """Schedule a file to become versioned."""
1442+ raise NotImplementedError(self.version_file)
1443+
1444+ def cancel_versioning(self, trans_id):
1445+ """Undo a previous versioning of a file"""
1446+ raise NotImplementedError(self.cancel_versioning)
1447+
1448+ def new_paths(self, filesystem_only=False):
1449+ """Determine the paths of all new and changed files.
1450+
1451+ :param filesystem_only: if True, only calculate values for files
1452+ that require renames or execute bit changes.
1453+ """
1454+ new_ids = set()
1455+ if filesystem_only:
1456+ stale_ids = self._needs_rename.difference(self._new_name)
1457+ stale_ids.difference_update(self._new_parent)
1458+ stale_ids.difference_update(self._new_contents)
1459+ stale_ids.difference_update(self._new_id)
1460+ needs_rename = self._needs_rename.difference(stale_ids)
1461+ id_sets = (needs_rename, self._new_executability)
1462+ else:
1463+ id_sets = (self._new_name, self._new_parent, self._new_contents,
1464+ self._new_id, self._new_executability)
1465+ for id_set in id_sets:
1466+ new_ids.update(id_set)
1467+ return sorted(FinalPaths(self).get_paths(new_ids))
1468+
1469+ def tree_file_id(self, trans_id):
1470+ """Determine the file id associated with the trans_id in the tree"""
1471+ path = self.tree_path(trans_id)
1472+ if path is None:
1473+ return None
1474+ # the file is old; the old id is still valid
1475+ if self._new_root == trans_id:
1476+ return self._tree.path2id('')
1477+ return self._tree.path2id(path)
1478+
1479+ def final_is_versioned(self, trans_id):
1480+ return self.final_file_id(trans_id) is not None
1481+
1482+ def final_file_id(self, trans_id):
1483+ """Determine the file id after any changes are applied, or None.
1484+
1485+ None indicates that the file will not be versioned after changes are
1486+ applied.
1487+ """
1488+ try:
1489+ return self._new_id[trans_id]
1490+ except KeyError:
1491+ if trans_id in self._removed_id:
1492+ return None
1493+ return self.tree_file_id(trans_id)
1494+
1495+ def inactive_file_id(self, trans_id):
1496+ """Return the inactive file_id associated with a transaction id.
1497+ That is, the one in the tree or in non_present_ids.
1498+ The file_id may actually be active, too.
1499+ """
1500+ file_id = self.tree_file_id(trans_id)
1501+ if file_id is not None:
1502+ return file_id
1503+ for key, value in viewitems(self._non_present_ids):
1504+ if value == trans_id:
1505+ return key
1506+
1507+ def find_conflicts(self):
1508+ """Find any violations of inventory or filesystem invariants"""
1509+ if self._done is True:
1510+ raise ReusingTransform()
1511+ conflicts = []
1512+ # ensure all children of all existent parents are known
1513+ # all children of non-existent parents are known, by definition.
1514+ self._add_tree_children()
1515+ by_parent = self.by_parent()
1516+ conflicts.extend(self._unversioned_parents(by_parent))
1517+ conflicts.extend(self._parent_loops())
1518+ conflicts.extend(self._duplicate_entries(by_parent))
1519+ conflicts.extend(self._parent_type_conflicts(by_parent))
1520+ conflicts.extend(self._improper_versioning())
1521+ conflicts.extend(self._executability_conflicts())
1522+ conflicts.extend(self._overwrite_conflicts())
1523+ return conflicts
1524+
1525+ def _check_malformed(self):
1526+ conflicts = self.find_conflicts()
1527+ if len(conflicts) != 0:
1528+ raise MalformedTransform(conflicts=conflicts)
1529+
1530+ def _add_tree_children(self):
1531+ """Add all the children of all active parents to the known paths.
1532+
1533+ Active parents are those which gain children, and those which are
1534+ removed. This is a necessary first step in detecting conflicts.
1535+ """
1536+ parents = list(self.by_parent())
1537+ parents.extend([t for t in self._removed_contents if
1538+ self.tree_kind(t) == 'directory'])
1539+ for trans_id in self._removed_id:
1540+ path = self.tree_path(trans_id)
1541+ if path is not None:
1542+ if self._tree.stored_kind(path) == 'directory':
1543+ parents.append(trans_id)
1544+ elif self.tree_kind(trans_id) == 'directory':
1545+ parents.append(trans_id)
1546+
1547+ for parent_id in parents:
1548+ # ensure that all children are registered with the transaction
1549+ list(self.iter_tree_children(parent_id))
1550+
1551+ def _has_named_child(self, name, parent_id, known_children):
1552+ """Does a parent already have a name child.
1553+
1554+ :param name: The searched for name.
1555+
1556+ :param parent_id: The parent for which the check is made.
1557+
1558+ :param known_children: The already known children. This should have
1559+ been recently obtained from `self.by_parent.get(parent_id)`
1560+ (or will be if None is passed).
1561+ """
1562+ if known_children is None:
1563+ known_children = self.by_parent().get(parent_id, [])
1564+ for child in known_children:
1565+ if self.final_name(child) == name:
1566+ return True
1567+ parent_path = self._tree_id_paths.get(parent_id, None)
1568+ if parent_path is None:
1569+ # No parent... no children
1570+ return False
1571+ child_path = joinpath(parent_path, name)
1572+ child_id = self._tree_path_ids.get(child_path, None)
1573+ if child_id is None:
1574+ # Not known by the tree transform yet, check the filesystem
1575+ return osutils.lexists(self._tree.abspath(child_path))
1576+ else:
1577+ raise AssertionError('child_id is missing: %s, %s, %s'
1578+ % (name, parent_id, child_id))
1579+
1580+ def _available_backup_name(self, name, target_id):
1581+ """Find an available backup name.
1582+
1583+ :param name: The basename of the file.
1584+
1585+ :param target_id: The directory trans_id where the backup should
1586+ be placed.
1587+ """
1588+ known_children = self.by_parent().get(target_id, [])
1589+ return osutils.available_backup_name(
1590+ name,
1591+ lambda base: self._has_named_child(
1592+ base, target_id, known_children))
1593+
1594+ def _parent_loops(self):
1595+ """No entry should be its own ancestor"""
1596+ conflicts = []
1597+ for trans_id in self._new_parent:
1598+ seen = set()
1599+ parent_id = trans_id
1600+ while parent_id != ROOT_PARENT:
1601+ seen.add(parent_id)
1602+ try:
1603+ parent_id = self.final_parent(parent_id)
1604+ except KeyError:
1605+ break
1606+ if parent_id == trans_id:
1607+ conflicts.append(('parent loop', trans_id))
1608+ if parent_id in seen:
1609+ break
1610+ return conflicts
1611+
1612+ def _unversioned_parents(self, by_parent):
1613+ """If parent directories are versioned, children must be versioned."""
1614+ conflicts = []
1615+ for parent_id, children in viewitems(by_parent):
1616+ if parent_id == ROOT_PARENT:
1617+ continue
1618+ if self.final_is_versioned(parent_id):
1619+ continue
1620+ for child_id in children:
1621+ if self.final_is_versioned(child_id):
1622+ conflicts.append(('unversioned parent', parent_id))
1623+ break
1624+ return conflicts
1625+
1626+ def _improper_versioning(self):
1627+ """Cannot version a file with no contents, or a bad type.
1628+
1629+ However, existing entries with no contents are okay.
1630+ """
1631+ conflicts = []
1632+ for trans_id in self._new_id:
1633+ kind = self.final_kind(trans_id)
1634+ if kind == 'symlink' and not self._tree.supports_symlinks():
1635+ # Ignore symlinks as they are not supported on this platform
1636+ continue
1637+ if kind is None:
1638+ conflicts.append(('versioning no contents', trans_id))
1639+ continue
1640+ if not self._tree.versionable_kind(kind):
1641+ conflicts.append(('versioning bad kind', trans_id, kind))
1642+ return conflicts
1643+
1644+ def _executability_conflicts(self):
1645+ """Check for bad executability changes.
1646+
1647+ Only versioned files may have their executability set, because
1648+ 1. only versioned entries can have executability under windows
1649+ 2. only files can be executable. (The execute bit on a directory
1650+ does not indicate searchability)
1651+ """
1652+ conflicts = []
1653+ for trans_id in self._new_executability:
1654+ if not self.final_is_versioned(trans_id):
1655+ conflicts.append(('unversioned executability', trans_id))
1656+ else:
1657+ if self.final_kind(trans_id) != "file":
1658+ conflicts.append(('non-file executability', trans_id))
1659+ return conflicts
1660+
1661+ def _overwrite_conflicts(self):
1662+ """Check for overwrites (not permitted on Win32)"""
1663+ conflicts = []
1664+ for trans_id in self._new_contents:
1665+ if self.tree_kind(trans_id) is None:
1666+ continue
1667+ if trans_id not in self._removed_contents:
1668+ conflicts.append(('overwrite', trans_id,
1669+ self.final_name(trans_id)))
1670+ return conflicts
1671+
1672+ def _duplicate_entries(self, by_parent):
1673+ """No directory may have two entries with the same name."""
1674+ conflicts = []
1675+ if (self._new_name, self._new_parent) == ({}, {}):
1676+ return conflicts
1677+ for children in viewvalues(by_parent):
1678+ name_ids = []
1679+ for child_tid in children:
1680+ name = self.final_name(child_tid)
1681+ if name is not None:
1682+ # Keep children only if they still exist in the end
1683+ if not self._case_sensitive_target:
1684+ name = name.lower()
1685+ name_ids.append((name, child_tid))
1686+ name_ids.sort()
1687+ last_name = None
1688+ last_trans_id = None
1689+ for name, trans_id in name_ids:
1690+ kind = self.final_kind(trans_id)
1691+ if kind is None and not self.final_is_versioned(trans_id):
1692+ continue
1693+ if name == last_name:
1694+ conflicts.append(('duplicate', last_trans_id, trans_id,
1695+ name))
1696+ last_name = name
1697+ last_trans_id = trans_id
1698+ return conflicts
1699+
1700+ def _parent_type_conflicts(self, by_parent):
1701+ """Children must have a directory parent"""
1702+ conflicts = []
1703+ for parent_id, children in viewitems(by_parent):
1704+ if parent_id == ROOT_PARENT:
1705+ continue
1706+ no_children = True
1707+ for child_id in children:
1708+ if self.final_kind(child_id) is not None:
1709+ no_children = False
1710+ break
1711+ if no_children:
1712+ continue
1713+ # There is at least a child, so we need an existing directory to
1714+ # contain it.
1715+ kind = self.final_kind(parent_id)
1716+ if kind is None:
1717+ # The directory will be deleted
1718+ conflicts.append(('missing parent', parent_id))
1719+ elif kind != "directory":
1720+ # Meh, we need a *directory* to put something in it
1721+ conflicts.append(('non-directory parent', parent_id))
1722+ return conflicts
1723+
1724+ def _set_executability(self, path, trans_id):
1725+ """Set the executability of versioned files """
1726+ if self._tree._supports_executable():
1727+ new_executability = self._new_executability[trans_id]
1728+ abspath = self._tree.abspath(path)
1729+ current_mode = os.stat(abspath).st_mode
1730+ if new_executability:
1731+ umask = os.umask(0)
1732+ os.umask(umask)
1733+ to_mode = current_mode | (0o100 & ~umask)
1734+ # Enable x-bit for others only if they can read it.
1735+ if current_mode & 0o004:
1736+ to_mode |= 0o001 & ~umask
1737+ if current_mode & 0o040:
1738+ to_mode |= 0o010 & ~umask
1739+ else:
1740+ to_mode = current_mode & ~0o111
1741+ osutils.chmod_if_possible(abspath, to_mode)
1742+
1743+ def _new_entry(self, name, parent_id, file_id):
1744+ """Helper function to create a new filesystem entry."""
1745+ trans_id = self.create_path(name, parent_id)
1746+ if file_id is not None:
1747+ self.version_file(trans_id, file_id=file_id)
1748+ return trans_id
1749+
1750+ def new_file(self, name, parent_id, contents, file_id=None,
1751+ executable=None, sha1=None):
1752+ """Convenience method to create files.
1753+
1754+ name is the name of the file to create.
1755+ parent_id is the transaction id of the parent directory of the file.
1756+ contents is an iterator of bytestrings, which will be used to produce
1757+ the file.
1758+ :param file_id: The inventory ID of the file, if it is to be versioned.
1759+ :param executable: Only valid when a file_id has been supplied.
1760+ """
1761+ trans_id = self._new_entry(name, parent_id, file_id)
1762+ # TODO: rather than scheduling a set_executable call,
1763+ # have create_file create the file with the right mode.
1764+ self.create_file(contents, trans_id, sha1=sha1)
1765+ if executable is not None:
1766+ self.set_executability(executable, trans_id)
1767+ return trans_id
1768+
1769+ def new_directory(self, name, parent_id, file_id=None):
1770+ """Convenience method to create directories.
1771+
1772+ name is the name of the directory to create.
1773+ parent_id is the transaction id of the parent directory of the
1774+ directory.
1775+ file_id is the inventory ID of the directory, if it is to be versioned.
1776+ """
1777+ trans_id = self._new_entry(name, parent_id, file_id)
1778+ self.create_directory(trans_id)
1779+ return trans_id
1780+
1781+ def new_symlink(self, name, parent_id, target, file_id=None):
1782+ """Convenience method to create symbolic link.
1783+
1784+ name is the name of the symlink to create.
1785+ parent_id is the transaction id of the parent directory of the symlink.
1786+ target is a bytestring of the target of the symlink.
1787+ file_id is the inventory ID of the file, if it is to be versioned.
1788+ """
1789+ trans_id = self._new_entry(name, parent_id, file_id)
1790+ self.create_symlink(target, trans_id)
1791+ return trans_id
1792+
1793+ def new_orphan(self, trans_id, parent_id):
1794+ """Schedule an item to be orphaned.
1795+
1796+ When a directory is about to be removed, its children, if they are not
1797+ versioned are moved out of the way: they don't have a parent anymore.
1798+
1799+ :param trans_id: The trans_id of the existing item.
1800+ :param parent_id: The parent trans_id of the item.
1801+ """
1802+ raise NotImplementedError(self.new_orphan)
1803+
1804+ def _get_potential_orphans(self, dir_id):
1805+ """Find the potential orphans in a directory.
1806+
1807+ A directory can't be safely deleted if there are versioned files in it.
1808+ If all the contained files are unversioned then they can be orphaned.
1809+
1810+ The 'None' return value means that the directory contains at least one
1811+ versioned file and should not be deleted.
1812+
1813+ :param dir_id: The directory trans id.
1814+
1815+ :return: A list of the orphan trans ids or None if at least one
1816+ versioned file is present.
1817+ """
1818+ orphans = []
1819+ # Find the potential orphans, stop if one item should be kept
1820+ for child_tid in self.by_parent()[dir_id]:
1821+ if child_tid in self._removed_contents:
1822+ # The child is removed as part of the transform. Since it was
1823+ # versioned before, it's not an orphan
1824+ continue
1825+ if not self.final_is_versioned(child_tid):
1826+ # The child is not versioned
1827+ orphans.append(child_tid)
1828+ else:
1829+ # We have a versioned file here, searching for orphans is
1830+ # meaningless.
1831+ orphans = None
1832+ break
1833+ return orphans
1834+
1835+ def _affected_ids(self):
1836+ """Return the set of transform ids affected by the transform"""
1837+ trans_ids = set(self._removed_id)
1838+ trans_ids.update(self._new_id)
1839+ trans_ids.update(self._removed_contents)
1840+ trans_ids.update(self._new_contents)
1841+ trans_ids.update(self._new_executability)
1842+ trans_ids.update(self._new_name)
1843+ trans_ids.update(self._new_parent)
1844+ return trans_ids
1845+
1846+ def _get_file_id_maps(self):
1847+ """Return mapping of file_ids to trans_ids in the to and from states"""
1848+ trans_ids = self._affected_ids()
1849+ from_trans_ids = {}
1850+ to_trans_ids = {}
1851+ # Build up two dicts: trans_ids associated with file ids in the
1852+ # FROM state, vs the TO state.
1853+ for trans_id in trans_ids:
1854+ from_file_id = self.tree_file_id(trans_id)
1855+ if from_file_id is not None:
1856+ from_trans_ids[from_file_id] = trans_id
1857+ to_file_id = self.final_file_id(trans_id)
1858+ if to_file_id is not None:
1859+ to_trans_ids[to_file_id] = trans_id
1860+ return from_trans_ids, to_trans_ids
1861+
1862+ def _from_file_data(self, from_trans_id, from_versioned, from_path):
1863+ """Get data about a file in the from (tree) state
1864+
1865+ Return a (name, parent, kind, executable) tuple
1866+ """
1867+ from_path = self._tree_id_paths.get(from_trans_id)
1868+ if from_versioned:
1869+ # get data from working tree if versioned
1870+ from_entry = next(self._tree.iter_entries_by_dir(
1871+ specific_files=[from_path]))[1]
1872+ from_name = from_entry.name
1873+ from_parent = from_entry.parent_id
1874+ else:
1875+ from_entry = None
1876+ if from_path is None:
1877+ # File does not exist in FROM state
1878+ from_name = None
1879+ from_parent = None
1880+ else:
1881+ # File exists, but is not versioned. Have to use path-
1882+ # splitting stuff
1883+ from_name = os.path.basename(from_path)
1884+ tree_parent = self.get_tree_parent(from_trans_id)
1885+ from_parent = self.tree_file_id(tree_parent)
1886+ if from_path is not None:
1887+ from_kind, from_executable, from_stats = \
1888+ self._tree._comparison_data(from_entry, from_path)
1889+ else:
1890+ from_kind = None
1891+ from_executable = False
1892+ return from_name, from_parent, from_kind, from_executable
1893+
1894+ def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1895+ """Get data about a file in the to (target) state
1896+
1897+ Return a (name, parent, kind, executable) tuple
1898+ """
1899+ to_name = self.final_name(to_trans_id)
1900+ to_kind = self.final_kind(to_trans_id)
1901+ to_parent = self.final_file_id(self.final_parent(to_trans_id))
1902+ if to_trans_id in self._new_executability:
1903+ to_executable = self._new_executability[to_trans_id]
1904+ elif to_trans_id == from_trans_id:
1905+ to_executable = from_executable
1906+ else:
1907+ to_executable = False
1908+ return to_name, to_parent, to_kind, to_executable
1909+
1910+ def iter_changes(self):
1911+ """Produce output in the same format as Tree.iter_changes.
1912+
1913+ Will produce nonsensical results if invoked while inventory/filesystem
1914+ conflicts (as reported by TreeTransform.find_conflicts()) are present.
1915+
1916+ This reads the Transform, but only reproduces changes involving a
1917+ file_id. Files that are not versioned in either of the FROM or TO
1918+ states are not reflected.
1919+ """
1920+ final_paths = FinalPaths(self)
1921+ from_trans_ids, to_trans_ids = self._get_file_id_maps()
1922+ results = []
1923+ # Now iterate through all active file_ids
1924+ for file_id in set(from_trans_ids).union(to_trans_ids):
1925+ modified = False
1926+ from_trans_id = from_trans_ids.get(file_id)
1927+ # find file ids, and determine versioning state
1928+ if from_trans_id is None:
1929+ from_versioned = False
1930+ from_trans_id = to_trans_ids[file_id]
1931+ else:
1932+ from_versioned = True
1933+ to_trans_id = to_trans_ids.get(file_id)
1934+ if to_trans_id is None:
1935+ to_versioned = False
1936+ to_trans_id = from_trans_id
1937+ else:
1938+ to_versioned = True
1939+
1940+ if not from_versioned:
1941+ from_path = None
1942+ else:
1943+ from_path = self._tree_id_paths.get(from_trans_id)
1944+ if not to_versioned:
1945+ to_path = None
1946+ else:
1947+ to_path = final_paths.get_path(to_trans_id)
1948+
1949+ from_name, from_parent, from_kind, from_executable = \
1950+ self._from_file_data(from_trans_id, from_versioned, from_path)
1951+
1952+ to_name, to_parent, to_kind, to_executable = \
1953+ self._to_file_data(to_trans_id, from_trans_id, from_executable)
1954+
1955+ if from_kind != to_kind:
1956+ modified = True
1957+ elif to_kind in ('file', 'symlink') and (
1958+ to_trans_id != from_trans_id
1959+ or to_trans_id in self._new_contents):
1960+ modified = True
1961+ if (not modified and from_versioned == to_versioned
1962+ and from_parent == to_parent and from_name == to_name
1963+ and from_executable == to_executable):
1964+ continue
1965+ results.append(
1966+ TreeChange(
1967+ file_id, (from_path, to_path), modified,
1968+ (from_versioned, to_versioned),
1969+ (from_parent, to_parent),
1970+ (from_name, to_name),
1971+ (from_kind, to_kind),
1972+ (from_executable, to_executable)))
1973+
1974+ def path_key(c):
1975+ return (c.path[0] or '', c.path[1] or '')
1976+ return iter(sorted(results, key=path_key))
1977+
1978+ def get_preview_tree(self):
1979+ """Return a tree representing the result of the transform.
1980+
1981+ The tree is a snapshot, and altering the TreeTransform will invalidate
1982+ it.
1983+ """
1984+ raise NotImplementedError(self.get_preview_tree)
1985+
1986+ def commit(self, branch, message, merge_parents=None, strict=False,
1987+ timestamp=None, timezone=None, committer=None, authors=None,
1988+ revprops=None, revision_id=None):
1989+ """Commit the result of this TreeTransform to a branch.
1990+
1991+ :param branch: The branch to commit to.
1992+ :param message: The message to attach to the commit.
1993+ :param merge_parents: Additional parent revision-ids specified by
1994+ pending merges.
1995+ :param strict: If True, abort the commit if there are unversioned
1996+ files.
1997+ :param timestamp: if not None, seconds-since-epoch for the time and
1998+ date. (May be a float.)
1999+ :param timezone: Optional timezone for timestamp, as an offset in
2000+ seconds.
2001+ :param committer: Optional committer in email-id format.
2002+ (e.g. "J Random Hacker <jrandom@example.com>")
2003+ :param authors: Optional list of authors in email-id format.
2004+ :param revprops: Optional dictionary of revision properties.
2005+ :param revision_id: Optional revision id. (Specifying a revision-id
2006+ may reduce performance for some non-native formats.)
2007+ :return: The revision_id of the revision committed.
2008+ """
2009+ self._check_malformed()
2010+ if strict:
2011+ unversioned = set(self._new_contents).difference(set(self._new_id))
2012+ for trans_id in unversioned:
2013+ if not self.final_is_versioned(trans_id):
2014+ raise errors.StrictCommitFailed()
2015+
2016+ revno, last_rev_id = branch.last_revision_info()
2017+ if last_rev_id == _mod_revision.NULL_REVISION:
2018+ if merge_parents is not None:
2019+ raise ValueError('Cannot supply merge parents for first'
2020+ ' commit.')
2021+ parent_ids = []
2022+ else:
2023+ parent_ids = [last_rev_id]
2024+ if merge_parents is not None:
2025+ parent_ids.extend(merge_parents)
2026+ if self._tree.get_revision_id() != last_rev_id:
2027+ raise ValueError('TreeTransform not based on branch basis: %s' %
2028+ self._tree.get_revision_id().decode('utf-8'))
2029+ from .. import commit
2030+ revprops = commit.Commit.update_revprops(revprops, branch, authors)
2031+ builder = branch.get_commit_builder(parent_ids,
2032+ timestamp=timestamp,
2033+ timezone=timezone,
2034+ committer=committer,
2035+ revprops=revprops,
2036+ revision_id=revision_id)
2037+ preview = self.get_preview_tree()
2038+ list(builder.record_iter_changes(preview, last_rev_id,
2039+ self.iter_changes()))
2040+ builder.finish_inventory()
2041+ revision_id = builder.commit(message)
2042+ branch.set_last_revision_info(revno + 1, revision_id)
2043+ return revision_id
2044+
2045+ def _text_parent(self, trans_id):
2046+ path = self.tree_path(trans_id)
2047+ try:
2048+ if path is None or self._tree.kind(path) != 'file':
2049+ return None
2050+ except errors.NoSuchFile:
2051+ return None
2052+ return path
2053+
2054+ def _get_parents_texts(self, trans_id):
2055+ """Get texts for compression parents of this file."""
2056+ path = self._text_parent(trans_id)
2057+ if path is None:
2058+ return ()
2059+ return (self._tree.get_file_text(path),)
2060+
2061+ def _get_parents_lines(self, trans_id):
2062+ """Get lines for compression parents of this file."""
2063+ path = self._text_parent(trans_id)
2064+ if path is None:
2065+ return ()
2066+ return (self._tree.get_file_lines(path),)
2067+
2068+ def serialize(self, serializer):
2069+ """Serialize this TreeTransform.
2070+
2071+ :param serializer: A Serialiser like pack.ContainerSerializer.
2072+ """
2073+ from .. import bencode
2074+ new_name = {k.encode('utf-8'): v.encode('utf-8')
2075+ for k, v in viewitems(self._new_name)}
2076+ new_parent = {k.encode('utf-8'): v.encode('utf-8')
2077+ for k, v in viewitems(self._new_parent)}
2078+ new_id = {k.encode('utf-8'): v
2079+ for k, v in viewitems(self._new_id)}
2080+ new_executability = {k.encode('utf-8'): int(v)
2081+ for k, v in viewitems(self._new_executability)}
2082+ tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
2083+ for k, v in viewitems(self._tree_path_ids)}
2084+ non_present_ids = {k: v.encode('utf-8')
2085+ for k, v in viewitems(self._non_present_ids)}
2086+ removed_contents = [trans_id.encode('utf-8')
2087+ for trans_id in self._removed_contents]
2088+ removed_id = [trans_id.encode('utf-8')
2089+ for trans_id in self._removed_id]
2090+ attribs = {
2091+ b'_id_number': self._id_number,
2092+ b'_new_name': new_name,
2093+ b'_new_parent': new_parent,
2094+ b'_new_executability': new_executability,
2095+ b'_new_id': new_id,
2096+ b'_tree_path_ids': tree_path_ids,
2097+ b'_removed_id': removed_id,
2098+ b'_removed_contents': removed_contents,
2099+ b'_non_present_ids': non_present_ids,
2100+ }
2101+ yield serializer.bytes_record(bencode.bencode(attribs),
2102+ ((b'attribs',),))
2103+ for trans_id, kind in sorted(viewitems(self._new_contents)):
2104+ if kind == 'file':
2105+ with open(self._limbo_name(trans_id), 'rb') as cur_file:
2106+ lines = cur_file.readlines()
2107+ parents = self._get_parents_lines(trans_id)
2108+ mpdiff = multiparent.MultiParent.from_lines(lines, parents)
2109+ content = b''.join(mpdiff.to_patch())
2110+ if kind == 'directory':
2111+ content = b''
2112+ if kind == 'symlink':
2113+ content = self._read_symlink_target(trans_id)
2114+ if not isinstance(content, bytes):
2115+ content = content.encode('utf-8')
2116+ yield serializer.bytes_record(
2117+ content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
2118+
2119+ def deserialize(self, records):
2120+ """Deserialize a stored TreeTransform.
2121+
2122+ :param records: An iterable of (names, content) tuples, as per
2123+ pack.ContainerPushParser.
2124+ """
2125+ from .. import bencode
2126+ names, content = next(records)
2127+ attribs = bencode.bdecode(content)
2128+ self._id_number = attribs[b'_id_number']
2129+ self._new_name = {k.decode('utf-8'): v.decode('utf-8')
2130+ for k, v in viewitems(attribs[b'_new_name'])}
2131+ self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
2132+ for k, v in viewitems(attribs[b'_new_parent'])}
2133+ self._new_executability = {
2134+ k.decode('utf-8'): bool(v)
2135+ for k, v in viewitems(attribs[b'_new_executability'])}
2136+ self._new_id = {k.decode('utf-8'): v
2137+ for k, v in viewitems(attribs[b'_new_id'])}
2138+ self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
2139+ self._tree_path_ids = {}
2140+ self._tree_id_paths = {}
2141+ for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
2142+ path = bytepath.decode('utf-8')
2143+ trans_id = trans_id.decode('utf-8')
2144+ self._tree_path_ids[path] = trans_id
2145+ self._tree_id_paths[trans_id] = path
2146+ self._removed_id = {trans_id.decode('utf-8')
2147+ for trans_id in attribs[b'_removed_id']}
2148+ self._removed_contents = set(
2149+ trans_id.decode('utf-8')
2150+ for trans_id in attribs[b'_removed_contents'])
2151+ self._non_present_ids = {
2152+ k: v.decode('utf-8')
2153+ for k, v in viewitems(attribs[b'_non_present_ids'])}
2154+ for ((trans_id, kind),), content in records:
2155+ trans_id = trans_id.decode('utf-8')
2156+ kind = kind.decode('ascii')
2157+ if kind == 'file':
2158+ mpdiff = multiparent.MultiParent.from_patch(content)
2159+ lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
2160+ self.create_file(lines, trans_id)
2161+ if kind == 'directory':
2162+ self.create_directory(trans_id)
2163+ if kind == 'symlink':
2164+ self.create_symlink(content.decode('utf-8'), trans_id)
2165+
2166+ def create_file(self, contents, trans_id, mode_id=None, sha1=None):
2167+ """Schedule creation of a new file.
2168+
2169+ :seealso: new_file.
2170+
2171+ :param contents: an iterator of strings, all of which will be written
2172+ to the target destination.
2173+ :param trans_id: TreeTransform handle
2174+ :param mode_id: If not None, force the mode of the target file to match
2175+ the mode of the object referenced by mode_id.
2176+ Otherwise, we will try to preserve mode bits of an existing file.
2177+ :param sha1: If the sha1 of this content is already known, pass it in.
2178+ We can use it to prevent future sha1 computations.
2179+ """
2180+ raise NotImplementedError(self.create_file)
2181+
2182+ def create_directory(self, trans_id):
2183+ """Schedule creation of a new directory.
2184+
2185+ See also new_directory.
2186+ """
2187+ raise NotImplementedError(self.create_directory)
2188+
2189+ def create_symlink(self, target, trans_id):
2190+ """Schedule creation of a new symbolic link.
2191+
2192+ target is a bytestring.
2193+ See also new_symlink.
2194+ """
2195+ raise NotImplementedError(self.create_symlink)
2196+
2197+ def create_hardlink(self, path, trans_id):
2198+ """Schedule creation of a hard link"""
2199+ raise NotImplementedError(self.create_hardlink)
2200+
2201+ def cancel_creation(self, trans_id):
2202+ """Cancel the creation of new file contents."""
2203+ raise NotImplementedError(self.cancel_creation)
2204+
2205+ def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
2206+ """Apply all changes to the inventory and filesystem.
2207+
2208+ If filesystem or inventory conflicts are present, MalformedTransform
2209+ will be thrown.
2210+
2211+ If apply succeeds, finalize is not necessary.
2212+
2213+ :param no_conflicts: if True, the caller guarantees there are no
2214+ conflicts, so no check is made.
2215+ :param precomputed_delta: An inventory delta to use instead of
2216+ calculating one.
2217+ :param _mover: Supply an alternate FileMover, for testing
2218+ """
2219+ raise NotImplementedError(self.apply)
2220+
2221+
2222+class DiskTreeTransform(TreeTransformBase):
2223+ """Tree transform storing its contents on disk."""
2224+
2225+ def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
2226+ """Constructor.
2227+ :param tree: The tree that will be transformed, but not necessarily
2228+ the output tree.
2229+ :param limbodir: A directory where new files can be stored until
2230+ they are installed in their proper places
2231+ :param pb: ignored
2232+ :param case_sensitive: If True, the target of the transform is
2233+ case sensitive, not just case preserving.
2234+ """
2235+ TreeTransformBase.__init__(self, tree, pb, case_sensitive)
2236+ self._limbodir = limbodir
2237+ self._deletiondir = None
2238+ # A mapping of transform ids to their limbo filename
2239+ self._limbo_files = {}
2240+ self._possibly_stale_limbo_files = set()
2241+ # A mapping of transform ids to a set of the transform ids of children
2242+ # that their limbo directory has
2243+ self._limbo_children = {}
2244+ # Map transform ids to maps of child filename to child transform id
2245+ self._limbo_children_names = {}
2246+ # List of transform ids that need to be renamed from limbo into place
2247+ self._needs_rename = set()
2248+ self._creation_mtime = None
2249+ self._create_symlinks = osutils.supports_symlinks(self._limbodir)
2250+
2251+ def finalize(self):
2252+ """Release the working tree lock, if held, clean up limbo dir.
2253+
2254+ This is required if apply has not been invoked, but can be invoked
2255+ even after apply.
2256+ """
2257+ if self._tree is None:
2258+ return
2259+ try:
2260+ limbo_paths = list(viewvalues(self._limbo_files))
2261+ limbo_paths.extend(self._possibly_stale_limbo_files)
2262+ limbo_paths.sort(reverse=True)
2263+ for path in limbo_paths:
2264+ try:
2265+ osutils.delete_any(path)
2266+ except OSError as e:
2267+ if e.errno != errno.ENOENT:
2268+ raise
2269+ # XXX: warn? perhaps we just got interrupted at an
2270+ # inconvenient moment, but perhaps files are disappearing
2271+ # from under us?
2272+ try:
2273+ osutils.delete_any(self._limbodir)
2274+ except OSError:
2275+ # We don't especially care *why* the dir is immortal.
2276+ raise ImmortalLimbo(self._limbodir)
2277+ try:
2278+ if self._deletiondir is not None:
2279+ osutils.delete_any(self._deletiondir)
2280+ except OSError:
2281+ raise errors.ImmortalPendingDeletion(self._deletiondir)
2282+ finally:
2283+ TreeTransformBase.finalize(self)
2284+
2285+ def _limbo_supports_executable(self):
2286+ """Check if the limbo path supports the executable bit."""
2287+ return osutils.supports_executable(self._limbodir)
2288+
2289+ def _limbo_name(self, trans_id):
2290+ """Generate the limbo name of a file"""
2291+ limbo_name = self._limbo_files.get(trans_id)
2292+ if limbo_name is None:
2293+ limbo_name = self._generate_limbo_path(trans_id)
2294+ self._limbo_files[trans_id] = limbo_name
2295+ return limbo_name
2296+
2297+ def _generate_limbo_path(self, trans_id):
2298+ """Generate a limbo path using the trans_id as the relative path.
2299+
2300+ This is suitable as a fallback, and when the transform should not be
2301+ sensitive to the path encoding of the limbo directory.
2302+ """
2303+ self._needs_rename.add(trans_id)
2304+ return osutils.pathjoin(self._limbodir, trans_id)
2305+
2306+ def adjust_path(self, name, parent, trans_id):
2307+ previous_parent = self._new_parent.get(trans_id)
2308+ previous_name = self._new_name.get(trans_id)
2309+ super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
2310+ if (trans_id in self._limbo_files
2311+ and trans_id not in self._needs_rename):
2312+ self._rename_in_limbo([trans_id])
2313+ if previous_parent != parent:
2314+ self._limbo_children[previous_parent].remove(trans_id)
2315+ if previous_parent != parent or previous_name != name:
2316+ del self._limbo_children_names[previous_parent][previous_name]
2317+
2318+ def _rename_in_limbo(self, trans_ids):
2319+ """Fix limbo names so that the right final path is produced.
2320+
2321+ This means we outsmarted ourselves-- we tried to avoid renaming
2322+ these files later by creating them with their final names in their
2323+ final parents. But now the previous name or parent is no longer
2324+ suitable, so we have to rename them.
2325+
2326+ Even for trans_ids that have no new contents, we must remove their
2327+ entries from _limbo_files, because they are now stale.
2328+ """
2329+ for trans_id in trans_ids:
2330+ old_path = self._limbo_files[trans_id]
2331+ self._possibly_stale_limbo_files.add(old_path)
2332+ del self._limbo_files[trans_id]
2333+ if trans_id not in self._new_contents:
2334+ continue
2335+ new_path = self._limbo_name(trans_id)
2336+ os.rename(old_path, new_path)
2337+ self._possibly_stale_limbo_files.remove(old_path)
2338+ for descendant in self._limbo_descendants(trans_id):
2339+ desc_path = self._limbo_files[descendant]
2340+ desc_path = new_path + desc_path[len(old_path):]
2341+ self._limbo_files[descendant] = desc_path
2342+
2343+ def _limbo_descendants(self, trans_id):
2344+ """Return the set of trans_ids whose limbo paths descend from this."""
2345+ descendants = set(self._limbo_children.get(trans_id, []))
2346+ for descendant in list(descendants):
2347+ descendants.update(self._limbo_descendants(descendant))
2348+ return descendants
2349+
2350+ def _set_mode(self, trans_id, mode_id, typefunc):
2351+ raise NotImplementedError(self._set_mode)
2352+
2353+ def create_file(self, contents, trans_id, mode_id=None, sha1=None):
2354+ """Schedule creation of a new file.
2355+
2356+ :seealso: new_file.
2357+
2358+ :param contents: an iterator of strings, all of which will be written
2359+ to the target destination.
2360+ :param trans_id: TreeTransform handle
2361+ :param mode_id: If not None, force the mode of the target file to match
2362+ the mode of the object referenced by mode_id.
2363+ Otherwise, we will try to preserve mode bits of an existing file.
2364+ :param sha1: If the sha1 of this content is already known, pass it in.
2365+ We can use it to prevent future sha1 computations.
2366+ """
2367+ name = self._limbo_name(trans_id)
2368+ with open(name, 'wb') as f:
2369+ unique_add(self._new_contents, trans_id, 'file')
2370+ f.writelines(contents)
2371+ self._set_mtime(name)
2372+ self._set_mode(trans_id, mode_id, S_ISREG)
2373+ # It is unfortunate we have to use lstat instead of fstat, but we just
2374+ # used utime and chmod on the file, so we need the accurate final
2375+ # details.
2376+ if sha1 is not None:
2377+ self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
2378+
2379+ def _read_symlink_target(self, trans_id):
2380+ return os.readlink(self._limbo_name(trans_id))
2381+
2382+ def _set_mtime(self, path):
2383+ """All files that are created get the same mtime.
2384+
2385+ This time is set by the first object to be created.
2386+ """
2387+ if self._creation_mtime is None:
2388+ self._creation_mtime = time.time()
2389+ os.utime(path, (self._creation_mtime, self._creation_mtime))
2390+
2391+ def create_hardlink(self, path, trans_id):
2392+ """Schedule creation of a hard link"""
2393+ name = self._limbo_name(trans_id)
2394+ try:
2395+ os.link(path, name)
2396+ except OSError as e:
2397+ if e.errno != errno.EPERM:
2398+ raise
2399+ raise errors.HardLinkNotSupported(path)
2400+ try:
2401+ unique_add(self._new_contents, trans_id, 'file')
2402+ except BaseException:
2403+ # Clean up the file, it never got registered so
2404+ # TreeTransform.finalize() won't clean it up.
2405+ os.unlink(name)
2406+ raise
2407+
2408+ def create_directory(self, trans_id):
2409+ """Schedule creation of a new directory.
2410+
2411+ See also new_directory.
2412+ """
2413+ os.mkdir(self._limbo_name(trans_id))
2414+ unique_add(self._new_contents, trans_id, 'directory')
2415+
2416+ def create_symlink(self, target, trans_id):
2417+ """Schedule creation of a new symbolic link.
2418+
2419+ target is a bytestring.
2420+ See also new_symlink.
2421+ """
2422+ if self._create_symlinks:
2423+ os.symlink(target, self._limbo_name(trans_id))
2424+ else:
2425+ try:
2426+ path = FinalPaths(self).get_path(trans_id)
2427+ except KeyError:
2428+ path = None
2429+ trace.warning(
2430+ 'Unable to create symlink "%s" on this filesystem.' % (path,))
2431+ # We add symlink to _new_contents even if they are unsupported
2432+ # and not created. These entries are subsequently used to avoid
2433+ # conflicts on platforms that don't support symlink
2434+ unique_add(self._new_contents, trans_id, 'symlink')
2435+
2436+ def cancel_creation(self, trans_id):
2437+ """Cancel the creation of new file contents."""
2438+ del self._new_contents[trans_id]
2439+ if trans_id in self._observed_sha1s:
2440+ del self._observed_sha1s[trans_id]
2441+ children = self._limbo_children.get(trans_id)
2442+ # if this is a limbo directory with children, move them before removing
2443+ # the directory
2444+ if children is not None:
2445+ self._rename_in_limbo(children)
2446+ del self._limbo_children[trans_id]
2447+ del self._limbo_children_names[trans_id]
2448+ osutils.delete_any(self._limbo_name(trans_id))
2449+
2450+ def new_orphan(self, trans_id, parent_id):
2451+ conf = self._tree.get_config_stack()
2452+ handle_orphan = conf.get('transform.orphan_policy')
2453+ handle_orphan(self, trans_id, parent_id)
2454+
2455+
2456 class GitTreeTransform(DiskTreeTransform):
2457 """Represent a tree transformation.
2458
2459
2460=== modified file 'breezy/git/tree.py'
2461--- breezy/git/tree.py 2020-07-05 13:18:03 +0000
2462+++ breezy/git/tree.py 2020-07-06 02:29:08 +0000
2463@@ -1639,7 +1639,7 @@
2464 return GitTreeTransform(self, pb=pb)
2465
2466 def preview_transform(self, pb=None):
2467- from ..transform import GitTransformPreview
2468+ from .transform import GitTransformPreview
2469 return GitTransformPreview(self, pb=pb)
2470
2471
2472
2473=== modified file 'breezy/tests/per_tree/test_transform.py'
2474--- breezy/tests/per_tree/test_transform.py 2020-07-05 00:03:01 +0000
2475+++ breezy/tests/per_tree/test_transform.py 2020-07-06 02:29:08 +0000
2476@@ -19,6 +19,7 @@
2477
2478 from ... import (
2479 revision as _mod_revision,
2480+ tests,
2481 trace,
2482 )
2483 from ...diff import show_diff_trees
2484@@ -43,13 +44,6 @@
2485
2486
2487
2488-A_ENTRY = (b'a-id', ('a', 'a'), True, (True, True),
2489- (b'TREE_ROOT', b'TREE_ROOT'), ('a', 'a'), ('file', 'file'),
2490- (False, False), False)
2491-ROOT_ENTRY = (b'TREE_ROOT', ('', ''), False, (True, True), (None, None),
2492- ('', ''), ('directory', 'directory'), (False, False), False)
2493-
2494-
2495 class TestTransformPreview(TestCaseWithTree):
2496
2497 def setUp(self):
2498@@ -60,8 +54,7 @@
2499 def create_tree(self):
2500 tree = self.make_branch_and_tree('.')
2501 self.build_tree_contents([('a', b'content 1')])
2502- tree.set_root_id(b'TREE_ROOT')
2503- tree.add('a', b'a-id')
2504+ tree.add('a')
2505 revid1 = tree.commit('rev1')
2506 return tree.branch.repository.revision_tree(revid1)
2507
2508@@ -110,10 +103,9 @@
2509 self.requireFeature(SymlinkFeature)
2510 tree = self.make_branch_and_tree('.')
2511 self.build_tree_contents([('a', 'content 1')])
2512- tree.set_root_id(b'TREE_ROOT')
2513 tree.add('a')
2514 os.symlink('a', 'foo')
2515- tree.add('foo', b'foo-id')
2516+ tree.add('foo')
2517 revid1 = tree.commit('rev1')
2518 revision_tree = tree.branch.repository.revision_tree(revid1)
2519 preview = revision_tree.preview_transform()
2520@@ -140,14 +132,14 @@
2521 self.addCleanup(preview.finalize)
2522 preview.new_file('a', preview.root, [b'content 2'])
2523 resolve_conflicts(preview)
2524- trans_id = preview.trans_id_file_id(b'a-id')
2525+ trans_id = preview.trans_id_tree_path('a')
2526 self.assertEqual('a.moved', preview.final_name(trans_id))
2527
2528 def get_tree_and_preview_tree(self):
2529 revision_tree = self.create_tree()
2530 preview = revision_tree.preview_transform()
2531 self.addCleanup(preview.finalize)
2532- a_trans_id = preview.trans_id_file_id(b'a-id')
2533+ a_trans_id = preview.trans_id_tree_path('a')
2534 preview.delete_contents(a_trans_id)
2535 preview.create_file([b'b content'], a_trans_id)
2536 preview_tree = preview.get_preview_tree()
2537@@ -156,7 +148,7 @@
2538 def test_iter_changes(self):
2539 revision_tree, preview_tree = self.get_tree_and_preview_tree()
2540 root = revision_tree.path2id('')
2541- self.assertEqual([(b'a-id', ('a', 'a'), True, (True, True),
2542+ self.assertEqual([(revision_tree.path2id('a'), ('a', 'a'), True, (True, True),
2543 (root, root), ('a', 'a'), ('file', 'file'),
2544 (False, False), False)],
2545 list(preview_tree.iter_changes(revision_tree)))
2546@@ -165,19 +157,39 @@
2547 revision_tree, preview_tree = self.get_tree_and_preview_tree()
2548 changes = preview_tree.iter_changes(revision_tree,
2549 include_unchanged=True)
2550- self.assertEqual([ROOT_ENTRY, A_ENTRY], list(changes))
2551+
2552+ root_id = revision_tree.path2id('')
2553+ root_entry = (root_id, ('', ''), False, (True, True), (None, None),
2554+ ('', ''), ('directory', 'directory'), (False, False), False)
2555+ a_entry = (revision_tree.path2id('a'), ('a', 'a'), True, (True, True),
2556+ (root_id, root_id), ('a', 'a'), ('file', 'file'),
2557+ (False, False), False)
2558+
2559+
2560+ self.assertEqual([root_entry, a_entry], list(changes))
2561
2562 def test_specific_files(self):
2563 revision_tree, preview_tree = self.get_tree_and_preview_tree()
2564 changes = preview_tree.iter_changes(revision_tree,
2565 specific_files=[''])
2566- self.assertEqual([A_ENTRY], list(changes))
2567+ root_id = revision_tree.path2id('')
2568+ a_entry = (revision_tree.path2id('a'), ('a', 'a'), True, (True, True),
2569+ (root_id, root_id), ('a', 'a'), ('file', 'file'),
2570+ (False, False), False)
2571+
2572+
2573+ self.assertEqual([a_entry], list(changes))
2574
2575 def test_want_unversioned(self):
2576 revision_tree, preview_tree = self.get_tree_and_preview_tree()
2577 changes = preview_tree.iter_changes(revision_tree,
2578 want_unversioned=True)
2579- self.assertEqual([A_ENTRY], list(changes))
2580+ root_id = revision_tree.path2id('')
2581+ a_entry = (revision_tree.path2id('a'), ('a', 'a'), True, (True, True),
2582+ (root_id, root_id), ('a', 'a'), ('file', 'file'),
2583+ (False, False), False)
2584+
2585+ self.assertEqual([a_entry], list(changes))
2586
2587 def test_ignore_extra_trees_no_specific_files(self):
2588 # extra_trees is harmless without specific_files, so we'll silently
2589@@ -217,7 +229,7 @@
2590 def test_get_file_mtime_renamed(self):
2591 work_tree = self.make_branch_and_tree('tree')
2592 self.build_tree(['tree/file'])
2593- work_tree.add('file', b'file-id')
2594+ work_tree.add('file')
2595 preview = work_tree.preview_transform()
2596 self.addCleanup(preview.finalize)
2597 file_trans_id = preview.trans_id_tree_path('file')
2598@@ -229,7 +241,7 @@
2599 def test_get_file_size(self):
2600 work_tree = self.make_branch_and_tree('tree')
2601 self.build_tree_contents([('tree/old', b'old')])
2602- work_tree.add('old', b'old-id')
2603+ work_tree.add('old')
2604 preview = work_tree.preview_transform()
2605 self.addCleanup(preview.finalize)
2606 preview.new_file('name', preview.root, [b'contents'], b'new-id',
2607@@ -254,6 +266,9 @@
2608 preview_tree.get_symlink_target('symlink'))
2609
2610 def test_all_file_ids(self):
2611+ if not self.workingtree_format.supports_setting_file_ids:
2612+ raise tests.TestNotApplicable(
2613+ 'format does not support setting file ids')
2614 tree = self.make_branch_and_tree('tree')
2615 self.build_tree(['tree/a', 'tree/b', 'tree/c'])
2616 tree.add(['a', 'b', 'c'], [b'a-id', b'b-id', b'c-id'])
2617@@ -294,30 +309,40 @@
2618 def test_path2id_moved(self):
2619 tree = self.make_branch_and_tree('tree')
2620 self.build_tree(['tree/old_parent/', 'tree/old_parent/child'])
2621- tree.add(['old_parent', 'old_parent/child'],
2622- [b'old_parent-id', b'child-id'])
2623+ tree.add(['old_parent', 'old_parent/child'])
2624 preview = tree.preview_transform()
2625 self.addCleanup(preview.finalize)
2626 new_parent = preview.new_directory('new_parent', preview.root,
2627 b'new_parent-id')
2628 preview.adjust_path('child', new_parent,
2629- preview.trans_id_file_id(b'child-id'))
2630+ preview.trans_id_tree_path('old_parent/child'))
2631 preview_tree = preview.get_preview_tree()
2632 self.assertFalse(preview_tree.is_versioned('old_parent/child'))
2633- self.assertEqual(b'child-id', preview_tree.path2id('new_parent/child'))
2634+ self.assertEqual(
2635+ 'new_parent/child',
2636+ find_previous_path(tree, preview_tree, 'old_parent/child'))
2637+ if self.workingtree_format.supports_setting_file_ids:
2638+ self.assertEqual(
2639+ tree.path2id('old_parent/child'),
2640+ preview_tree.path2id('new_parent/child'))
2641
2642 def test_path2id_renamed_parent(self):
2643 tree = self.make_branch_and_tree('tree')
2644 self.build_tree(['tree/old_name/', 'tree/old_name/child'])
2645- tree.add(['old_name', 'old_name/child'],
2646- [b'parent-id', b'child-id'])
2647+ tree.add(['old_name', 'old_name/child'])
2648 preview = tree.preview_transform()
2649 self.addCleanup(preview.finalize)
2650 preview.adjust_path('new_name', preview.root,
2651- preview.trans_id_file_id(b'parent-id'))
2652+ preview.trans_id_tree_path('old_name'))
2653 preview_tree = preview.get_preview_tree()
2654 self.assertFalse(preview_tree.is_versioned('old_name/child'))
2655- self.assertEqual(b'child-id', preview_tree.path2id('new_name/child'))
2656+ self.assertEqual(
2657+ 'new_name/child',
2658+ find_previous_path(tree, preview_tree, 'old_name/child'))
2659+ if tree.supports_setting_file_ids:
2660+ self.assertEqual(
2661+ tree.path2id('old_name/child'),
2662+ preview_tree.path2id('new_name/child'))
2663
2664 def assertMatchingIterEntries(self, tt, specific_files=None):
2665 preview_tree = tt.get_preview_tree()
2666@@ -338,33 +363,33 @@
2667 def test_iter_entries_by_dir_deleted(self):
2668 tree = self.make_branch_and_tree('tree')
2669 self.build_tree(['tree/deleted'])
2670- tree.add('deleted', b'deleted-id')
2671+ tree.add('deleted')
2672 tt = tree.transform()
2673- tt.delete_contents(tt.trans_id_file_id(b'deleted-id'))
2674+ tt.delete_contents(tt.trans_id_tree_path('deleted'))
2675 self.assertMatchingIterEntries(tt)
2676
2677 def test_iter_entries_by_dir_unversioned(self):
2678 tree = self.make_branch_and_tree('tree')
2679 self.build_tree(['tree/removed'])
2680- tree.add('removed', b'removed-id')
2681+ tree.add('removed')
2682 tt = tree.transform()
2683- tt.unversion_file(tt.trans_id_file_id(b'removed-id'))
2684+ tt.unversion_file(tt.trans_id_tree_path('removed'))
2685 self.assertMatchingIterEntries(tt)
2686
2687 def test_iter_entries_by_dir_moved(self):
2688 tree = self.make_branch_and_tree('tree')
2689 self.build_tree(['tree/moved', 'tree/new_parent/'])
2690- tree.add(['moved', 'new_parent'], [b'moved-id', b'new_parent-id'])
2691+ tree.add(['moved', 'new_parent'])
2692 tt = tree.transform()
2693- tt.adjust_path('moved', tt.trans_id_file_id(b'new_parent-id'),
2694- tt.trans_id_file_id(b'moved-id'))
2695+ tt.adjust_path(
2696+ 'moved', tt.trans_id_tree_path('new_parent'),
2697+ tt.trans_id_tree_path('moved'))
2698 self.assertMatchingIterEntries(tt)
2699
2700 def test_iter_entries_by_dir_specific_files(self):
2701 tree = self.make_branch_and_tree('tree')
2702- tree.set_root_id(b'tree-root-id')
2703 self.build_tree(['tree/parent/', 'tree/parent/child'])
2704- tree.add(['parent', 'parent/child'], [b'parent-id', b'child-id'])
2705+ tree.add(['parent', 'parent/child'])
2706 tt = tree.transform()
2707 self.assertMatchingIterEntries(tt, ['', 'parent/child'])
2708
2709@@ -446,12 +471,12 @@
2710 def test_annotate(self):
2711 tree = self.make_branch_and_tree('tree')
2712 self.build_tree_contents([('tree/file', b'a\n')])
2713- tree.add('file', b'file-id')
2714+ tree.add('file')
2715 revid1 = tree.commit('a')
2716 self.build_tree_contents([('tree/file', b'a\nb\n')])
2717 preview = tree.preview_transform()
2718 self.addCleanup(preview.finalize)
2719- file_trans_id = preview.trans_id_file_id(b'file-id')
2720+ file_trans_id = preview.trans_id_tree_path('file')
2721 preview.delete_contents(file_trans_id)
2722 preview.create_file([b'a\nb\nc\n'], file_trans_id)
2723 preview_tree = preview.get_preview_tree()
2724@@ -636,12 +661,14 @@
2725
2726 def test_merge_preview_into_workingtree(self):
2727 tree = self.make_branch_and_tree('tree')
2728- tree.set_root_id(b'TREE_ROOT')
2729+ if tree.supports_setting_file_ids():
2730+ tree.set_root_id(b'TREE_ROOT')
2731 tt = tree.preview_transform()
2732 self.addCleanup(tt.finalize)
2733 tt.new_file('name', tt.root, [b'content'], b'file-id')
2734 tree2 = self.make_branch_and_tree('tree2')
2735- tree2.set_root_id(b'TREE_ROOT')
2736+ if tree.supports_setting_file_ids():
2737+ tree2.set_root_id(b'TREE_ROOT')
2738 merger = Merger.from_uncommitted(tree2, tt.get_preview_tree(),
2739 tree.basis_tree())
2740 merger.merge_type = Merge3Merger
2741@@ -650,11 +677,11 @@
2742 def test_merge_preview_into_workingtree_handles_conflicts(self):
2743 tree = self.make_branch_and_tree('tree')
2744 self.build_tree_contents([('tree/foo', b'bar')])
2745- tree.add('foo', b'foo-id')
2746+ tree.add('foo')
2747 tree.commit('foo')
2748 tt = tree.preview_transform()
2749 self.addCleanup(tt.finalize)
2750- trans_id = tt.trans_id_file_id(b'foo-id')
2751+ trans_id = tt.trans_id_tree_path('foo')
2752 tt.delete_contents(trans_id)
2753 tt.create_file([b'baz'], trans_id)
2754 tree2 = tree.controldir.sprout('tree2').open_workingtree()
2755@@ -682,7 +709,7 @@
2756 self.assertTrue(tree.has_filename('new'))
2757 self.assertTrue(tree.has_filename('modified'))
2758
2759- def test_is_executable(self):
2760+ def test_is_executable2(self):
2761 tree = self.make_branch_and_tree('tree')
2762 preview = tree.preview_transform()
2763 self.addCleanup(preview.finalize)
2764
2765=== modified file 'breezy/tests/per_workingtree/test_transform.py'
2766--- breezy/tests/per_workingtree/test_transform.py 2020-07-05 17:49:39 +0000
2767+++ breezy/tests/per_workingtree/test_transform.py 2020-07-06 02:29:08 +0000
2768@@ -256,6 +256,9 @@
2769 self.assertEqual(st1.st_mtime, st2.st_mtime)
2770
2771 def test_change_root_id(self):
2772+ if not self.workingtree_format.supports_setting_file_ids:
2773+ raise tests.TestNotApplicable(
2774+ 'format does not support setting file ids')
2775 transform, root = self.transform()
2776 self.assertNotEqual(b'new-root-id', self.wt.path2id(''))
2777 transform.new_directory('', ROOT_PARENT, b'new-root-id')
2778@@ -265,7 +268,18 @@
2779 transform.apply()
2780 self.assertEqual(b'new-root-id', self.wt.path2id(''))
2781
2782+ def test_replace_root(self):
2783+ transform, root = self.transform()
2784+ transform.new_directory('', ROOT_PARENT, b'new-root-id')
2785+ transform.delete_contents(root)
2786+ transform.unversion_file(root)
2787+ transform.fixup_new_roots()
2788+ transform.apply()
2789+
2790 def test_change_root_id_add_files(self):
2791+ if not self.workingtree_format.supports_setting_file_ids:
2792+ raise tests.TestNotApplicable(
2793+ 'format does not support setting file ids')
2794 transform, root = self.transform()
2795 self.assertNotEqual(b'new-root-id', self.wt.path2id(''))
2796 new_trans_id = transform.new_directory('', ROOT_PARENT, b'new-root-id')
2797
2798=== modified file 'breezy/transform.py'
2799--- breezy/transform.py 2020-07-05 19:02:14 +0000
2800+++ breezy/transform.py 2020-07-06 02:29:08 +0000
2801@@ -531,1192 +531,6 @@
2802 raise NotImplementedError(self.cancel_creation)
2803
2804
2805-class TreeTransformBase(TreeTransform):
2806- """The base class for TreeTransform and its kin."""
2807-
2808- def __init__(self, tree, pb=None, case_sensitive=True):
2809- """Constructor.
2810-
2811- :param tree: The tree that will be transformed, but not necessarily
2812- the output tree.
2813- :param pb: ignored
2814- :param case_sensitive: If True, the target of the transform is
2815- case sensitive, not just case preserving.
2816- """
2817- super(TreeTransformBase, self).__init__(tree, pb=pb)
2818- # mapping of trans_id => (sha1 of content, stat_value)
2819- self._observed_sha1s = {}
2820- # Mapping of trans_id -> new file_id
2821- self._new_id = {}
2822- # Mapping of old file-id -> trans_id
2823- self._non_present_ids = {}
2824- # Mapping of new file_id -> trans_id
2825- self._r_new_id = {}
2826- # The trans_id that will be used as the tree root
2827- if tree.is_versioned(''):
2828- self._new_root = self.trans_id_tree_path('')
2829- else:
2830- self._new_root = None
2831- # Whether the target is case sensitive
2832- self._case_sensitive_target = case_sensitive
2833-
2834- def finalize(self):
2835- """Release the working tree lock, if held.
2836-
2837- This is required if apply has not been invoked, but can be invoked
2838- even after apply.
2839- """
2840- if self._tree is None:
2841- return
2842- for hook in MutableTree.hooks['post_transform']:
2843- hook(self._tree, self)
2844- self._tree.unlock()
2845- self._tree = None
2846-
2847- def __get_root(self):
2848- return self._new_root
2849-
2850- root = property(__get_root)
2851-
2852- def create_path(self, name, parent):
2853- """Assign a transaction id to a new path"""
2854- trans_id = self._assign_id()
2855- unique_add(self._new_name, trans_id, name)
2856- unique_add(self._new_parent, trans_id, parent)
2857- return trans_id
2858-
2859- def adjust_root_path(self, name, parent):
2860- """Emulate moving the root by moving all children, instead.
2861-
2862- We do this by undoing the association of root's transaction id with the
2863- current tree. This allows us to create a new directory with that
2864- transaction id. We unversion the root directory and version the
2865- physically new directory, and hope someone versions the tree root
2866- later.
2867- """
2868- old_root = self._new_root
2869- old_root_file_id = self.final_file_id(old_root)
2870- # force moving all children of root
2871- for child_id in self.iter_tree_children(old_root):
2872- if child_id != parent:
2873- self.adjust_path(self.final_name(child_id),
2874- self.final_parent(child_id), child_id)
2875- file_id = self.final_file_id(child_id)
2876- if file_id is not None:
2877- self.unversion_file(child_id)
2878- self.version_file(child_id, file_id=file_id)
2879-
2880- # the physical root needs a new transaction id
2881- self._tree_path_ids.pop("")
2882- self._tree_id_paths.pop(old_root)
2883- self._new_root = self.trans_id_tree_path('')
2884- if parent == old_root:
2885- parent = self._new_root
2886- self.adjust_path(name, parent, old_root)
2887- self.create_directory(old_root)
2888- self.version_file(old_root, file_id=old_root_file_id)
2889- self.unversion_file(self._new_root)
2890-
2891- def fixup_new_roots(self):
2892- """Reinterpret requests to change the root directory
2893-
2894- Instead of creating a root directory, or moving an existing directory,
2895- all the attributes and children of the new root are applied to the
2896- existing root directory.
2897-
2898- This means that the old root trans-id becomes obsolete, so it is
2899- recommended only to invoke this after the root trans-id has become
2900- irrelevant.
2901-
2902- """
2903- new_roots = [k for k, v in viewitems(self._new_parent)
2904- if v == ROOT_PARENT]
2905- if len(new_roots) < 1:
2906- return
2907- if len(new_roots) != 1:
2908- raise ValueError('A tree cannot have two roots!')
2909- if self._new_root is None:
2910- self._new_root = new_roots[0]
2911- return
2912- old_new_root = new_roots[0]
2913- # unversion the new root's directory.
2914- if self.final_kind(self._new_root) is None:
2915- file_id = self.final_file_id(old_new_root)
2916- else:
2917- file_id = self.final_file_id(self._new_root)
2918- if old_new_root in self._new_id:
2919- self.cancel_versioning(old_new_root)
2920- else:
2921- self.unversion_file(old_new_root)
2922- # if, at this stage, root still has an old file_id, zap it so we can
2923- # stick a new one in.
2924- if (self.tree_file_id(self._new_root) is not None
2925- and self._new_root not in self._removed_id):
2926- self.unversion_file(self._new_root)
2927- if file_id is not None:
2928- self.version_file(self._new_root, file_id=file_id)
2929-
2930- # Now move children of new root into old root directory.
2931- # Ensure all children are registered with the transaction, but don't
2932- # use directly-- some tree children have new parents
2933- list(self.iter_tree_children(old_new_root))
2934- # Move all children of new root into old root directory.
2935- for child in self.by_parent().get(old_new_root, []):
2936- self.adjust_path(self.final_name(child), self._new_root, child)
2937-
2938- # Ensure old_new_root has no directory.
2939- if old_new_root in self._new_contents:
2940- self.cancel_creation(old_new_root)
2941- else:
2942- self.delete_contents(old_new_root)
2943-
2944- # prevent deletion of root directory.
2945- if self._new_root in self._removed_contents:
2946- self.cancel_deletion(self._new_root)
2947-
2948- # destroy path info for old_new_root.
2949- del self._new_parent[old_new_root]
2950- del self._new_name[old_new_root]
2951-
2952- def trans_id_file_id(self, file_id):
2953- """Determine or set the transaction id associated with a file ID.
2954- A new id is only created for file_ids that were never present. If
2955- a transaction has been unversioned, it is deliberately still returned.
2956- (this will likely lead to an unversioned parent conflict.)
2957- """
2958- if file_id is None:
2959- raise ValueError('None is not a valid file id')
2960- if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
2961- return self._r_new_id[file_id]
2962- else:
2963- try:
2964- path = self._tree.id2path(file_id)
2965- except errors.NoSuchId:
2966- if file_id in self._non_present_ids:
2967- return self._non_present_ids[file_id]
2968- else:
2969- trans_id = self._assign_id()
2970- self._non_present_ids[file_id] = trans_id
2971- return trans_id
2972- else:
2973- return self.trans_id_tree_path(path)
2974-
2975- def version_file(self, trans_id, file_id=None):
2976- """Schedule a file to become versioned."""
2977- raise NotImplementedError(self.version_file)
2978-
2979- def cancel_versioning(self, trans_id):
2980- """Undo a previous versioning of a file"""
2981- raise NotImplementedError(self.cancel_versioning)
2982-
2983- def new_paths(self, filesystem_only=False):
2984- """Determine the paths of all new and changed files.
2985-
2986- :param filesystem_only: if True, only calculate values for files
2987- that require renames or execute bit changes.
2988- """
2989- new_ids = set()
2990- if filesystem_only:
2991- stale_ids = self._needs_rename.difference(self._new_name)
2992- stale_ids.difference_update(self._new_parent)
2993- stale_ids.difference_update(self._new_contents)
2994- stale_ids.difference_update(self._new_id)
2995- needs_rename = self._needs_rename.difference(stale_ids)
2996- id_sets = (needs_rename, self._new_executability)
2997- else:
2998- id_sets = (self._new_name, self._new_parent, self._new_contents,
2999- self._new_id, self._new_executability)
3000- for id_set in id_sets:
3001- new_ids.update(id_set)
3002- return sorted(FinalPaths(self).get_paths(new_ids))
3003-
3004- def tree_file_id(self, trans_id):
3005- """Determine the file id associated with the trans_id in the tree"""
3006- path = self.tree_path(trans_id)
3007- if path is None:
3008- return None
3009- # the file is old; the old id is still valid
3010- if self._new_root == trans_id:
3011- return self._tree.path2id('')
3012- return self._tree.path2id(path)
3013-
3014- def final_is_versioned(self, trans_id):
3015- return self.final_file_id(trans_id) is not None
3016-
3017- def final_file_id(self, trans_id):
3018- """Determine the file id after any changes are applied, or None.
3019-
3020- None indicates that the file will not be versioned after changes are
3021- applied.
3022- """
3023- try:
3024- return self._new_id[trans_id]
3025- except KeyError:
3026- if trans_id in self._removed_id:
3027- return None
3028- return self.tree_file_id(trans_id)
3029-
3030- def inactive_file_id(self, trans_id):
3031- """Return the inactive file_id associated with a transaction id.
3032- That is, the one in the tree or in non_present_ids.
3033- The file_id may actually be active, too.
3034- """
3035- file_id = self.tree_file_id(trans_id)
3036- if file_id is not None:
3037- return file_id
3038- for key, value in viewitems(self._non_present_ids):
3039- if value == trans_id:
3040- return key
3041-
3042- def find_conflicts(self):
3043- """Find any violations of inventory or filesystem invariants"""
3044- if self._done is True:
3045- raise ReusingTransform()
3046- conflicts = []
3047- # ensure all children of all existent parents are known
3048- # all children of non-existent parents are known, by definition.
3049- self._add_tree_children()
3050- by_parent = self.by_parent()
3051- conflicts.extend(self._unversioned_parents(by_parent))
3052- conflicts.extend(self._parent_loops())
3053- conflicts.extend(self._duplicate_entries(by_parent))
3054- conflicts.extend(self._parent_type_conflicts(by_parent))
3055- conflicts.extend(self._improper_versioning())
3056- conflicts.extend(self._executability_conflicts())
3057- conflicts.extend(self._overwrite_conflicts())
3058- return conflicts
3059-
3060- def _check_malformed(self):
3061- conflicts = self.find_conflicts()
3062- if len(conflicts) != 0:
3063- raise MalformedTransform(conflicts=conflicts)
3064-
3065- def _add_tree_children(self):
3066- """Add all the children of all active parents to the known paths.
3067-
3068- Active parents are those which gain children, and those which are
3069- removed. This is a necessary first step in detecting conflicts.
3070- """
3071- parents = list(self.by_parent())
3072- parents.extend([t for t in self._removed_contents if
3073- self.tree_kind(t) == 'directory'])
3074- for trans_id in self._removed_id:
3075- path = self.tree_path(trans_id)
3076- if path is not None:
3077- if self._tree.stored_kind(path) == 'directory':
3078- parents.append(trans_id)
3079- elif self.tree_kind(trans_id) == 'directory':
3080- parents.append(trans_id)
3081-
3082- for parent_id in parents:
3083- # ensure that all children are registered with the transaction
3084- list(self.iter_tree_children(parent_id))
3085-
3086- def _has_named_child(self, name, parent_id, known_children):
3087- """Does a parent already have a name child.
3088-
3089- :param name: The searched for name.
3090-
3091- :param parent_id: The parent for which the check is made.
3092-
3093- :param known_children: The already known children. This should have
3094- been recently obtained from `self.by_parent.get(parent_id)`
3095- (or will be if None is passed).
3096- """
3097- if known_children is None:
3098- known_children = self.by_parent().get(parent_id, [])
3099- for child in known_children:
3100- if self.final_name(child) == name:
3101- return True
3102- parent_path = self._tree_id_paths.get(parent_id, None)
3103- if parent_path is None:
3104- # No parent... no children
3105- return False
3106- child_path = joinpath(parent_path, name)
3107- child_id = self._tree_path_ids.get(child_path, None)
3108- if child_id is None:
3109- # Not known by the tree transform yet, check the filesystem
3110- return osutils.lexists(self._tree.abspath(child_path))
3111- else:
3112- raise AssertionError('child_id is missing: %s, %s, %s'
3113- % (name, parent_id, child_id))
3114-
3115- def _available_backup_name(self, name, target_id):
3116- """Find an available backup name.
3117-
3118- :param name: The basename of the file.
3119-
3120- :param target_id: The directory trans_id where the backup should
3121- be placed.
3122- """
3123- known_children = self.by_parent().get(target_id, [])
3124- return osutils.available_backup_name(
3125- name,
3126- lambda base: self._has_named_child(
3127- base, target_id, known_children))
3128-
3129- def _parent_loops(self):
3130- """No entry should be its own ancestor"""
3131- conflicts = []
3132- for trans_id in self._new_parent:
3133- seen = set()
3134- parent_id = trans_id
3135- while parent_id != ROOT_PARENT:
3136- seen.add(parent_id)
3137- try:
3138- parent_id = self.final_parent(parent_id)
3139- except KeyError:
3140- break
3141- if parent_id == trans_id:
3142- conflicts.append(('parent loop', trans_id))
3143- if parent_id in seen:
3144- break
3145- return conflicts
3146-
3147- def _unversioned_parents(self, by_parent):
3148- """If parent directories are versioned, children must be versioned."""
3149- conflicts = []
3150- for parent_id, children in viewitems(by_parent):
3151- if parent_id == ROOT_PARENT:
3152- continue
3153- if self.final_is_versioned(parent_id):
3154- continue
3155- for child_id in children:
3156- if self.final_is_versioned(child_id):
3157- conflicts.append(('unversioned parent', parent_id))
3158- break
3159- return conflicts
3160-
3161- def _improper_versioning(self):
3162- """Cannot version a file with no contents, or a bad type.
3163-
3164- However, existing entries with no contents are okay.
3165- """
3166- conflicts = []
3167- for trans_id in self._new_id:
3168- kind = self.final_kind(trans_id)
3169- if kind == 'symlink' and not self._tree.supports_symlinks():
3170- # Ignore symlinks as they are not supported on this platform
3171- continue
3172- if kind is None:
3173- conflicts.append(('versioning no contents', trans_id))
3174- continue
3175- if not self._tree.versionable_kind(kind):
3176- conflicts.append(('versioning bad kind', trans_id, kind))
3177- return conflicts
3178-
3179- def _executability_conflicts(self):
3180- """Check for bad executability changes.
3181-
3182- Only versioned files may have their executability set, because
3183- 1. only versioned entries can have executability under windows
3184- 2. only files can be executable. (The execute bit on a directory
3185- does not indicate searchability)
3186- """
3187- conflicts = []
3188- for trans_id in self._new_executability:
3189- if not self.final_is_versioned(trans_id):
3190- conflicts.append(('unversioned executability', trans_id))
3191- else:
3192- if self.final_kind(trans_id) != "file":
3193- conflicts.append(('non-file executability', trans_id))
3194- return conflicts
3195-
3196- def _overwrite_conflicts(self):
3197- """Check for overwrites (not permitted on Win32)"""
3198- conflicts = []
3199- for trans_id in self._new_contents:
3200- if self.tree_kind(trans_id) is None:
3201- continue
3202- if trans_id not in self._removed_contents:
3203- conflicts.append(('overwrite', trans_id,
3204- self.final_name(trans_id)))
3205- return conflicts
3206-
3207- def _duplicate_entries(self, by_parent):
3208- """No directory may have two entries with the same name."""
3209- conflicts = []
3210- if (self._new_name, self._new_parent) == ({}, {}):
3211- return conflicts
3212- for children in viewvalues(by_parent):
3213- name_ids = []
3214- for child_tid in children:
3215- name = self.final_name(child_tid)
3216- if name is not None:
3217- # Keep children only if they still exist in the end
3218- if not self._case_sensitive_target:
3219- name = name.lower()
3220- name_ids.append((name, child_tid))
3221- name_ids.sort()
3222- last_name = None
3223- last_trans_id = None
3224- for name, trans_id in name_ids:
3225- kind = self.final_kind(trans_id)
3226- if kind is None and not self.final_is_versioned(trans_id):
3227- continue
3228- if name == last_name:
3229- conflicts.append(('duplicate', last_trans_id, trans_id,
3230- name))
3231- last_name = name
3232- last_trans_id = trans_id
3233- return conflicts
3234-
3235- def _parent_type_conflicts(self, by_parent):
3236- """Children must have a directory parent"""
3237- conflicts = []
3238- for parent_id, children in viewitems(by_parent):
3239- if parent_id == ROOT_PARENT:
3240- continue
3241- no_children = True
3242- for child_id in children:
3243- if self.final_kind(child_id) is not None:
3244- no_children = False
3245- break
3246- if no_children:
3247- continue
3248- # There is at least a child, so we need an existing directory to
3249- # contain it.
3250- kind = self.final_kind(parent_id)
3251- if kind is None:
3252- # The directory will be deleted
3253- conflicts.append(('missing parent', parent_id))
3254- elif kind != "directory":
3255- # Meh, we need a *directory* to put something in it
3256- conflicts.append(('non-directory parent', parent_id))
3257- return conflicts
3258-
3259- def _set_executability(self, path, trans_id):
3260- """Set the executability of versioned files """
3261- if self._tree._supports_executable():
3262- new_executability = self._new_executability[trans_id]
3263- abspath = self._tree.abspath(path)
3264- current_mode = os.stat(abspath).st_mode
3265- if new_executability:
3266- umask = os.umask(0)
3267- os.umask(umask)
3268- to_mode = current_mode | (0o100 & ~umask)
3269- # Enable x-bit for others only if they can read it.
3270- if current_mode & 0o004:
3271- to_mode |= 0o001 & ~umask
3272- if current_mode & 0o040:
3273- to_mode |= 0o010 & ~umask
3274- else:
3275- to_mode = current_mode & ~0o111
3276- osutils.chmod_if_possible(abspath, to_mode)
3277-
3278- def _new_entry(self, name, parent_id, file_id):
3279- """Helper function to create a new filesystem entry."""
3280- trans_id = self.create_path(name, parent_id)
3281- if file_id is not None:
3282- self.version_file(trans_id, file_id=file_id)
3283- return trans_id
3284-
3285- def new_file(self, name, parent_id, contents, file_id=None,
3286- executable=None, sha1=None):
3287- """Convenience method to create files.
3288-
3289- name is the name of the file to create.
3290- parent_id is the transaction id of the parent directory of the file.
3291- contents is an iterator of bytestrings, which will be used to produce
3292- the file.
3293- :param file_id: The inventory ID of the file, if it is to be versioned.
3294- :param executable: Only valid when a file_id has been supplied.
3295- """
3296- trans_id = self._new_entry(name, parent_id, file_id)
3297- # TODO: rather than scheduling a set_executable call,
3298- # have create_file create the file with the right mode.
3299- self.create_file(contents, trans_id, sha1=sha1)
3300- if executable is not None:
3301- self.set_executability(executable, trans_id)
3302- return trans_id
3303-
3304- def new_directory(self, name, parent_id, file_id=None):
3305- """Convenience method to create directories.
3306-
3307- name is the name of the directory to create.
3308- parent_id is the transaction id of the parent directory of the
3309- directory.
3310- file_id is the inventory ID of the directory, if it is to be versioned.
3311- """
3312- trans_id = self._new_entry(name, parent_id, file_id)
3313- self.create_directory(trans_id)
3314- return trans_id
3315-
3316- def new_symlink(self, name, parent_id, target, file_id=None):
3317- """Convenience method to create symbolic link.
3318-
3319- name is the name of the symlink to create.
3320- parent_id is the transaction id of the parent directory of the symlink.
3321- target is a bytestring of the target of the symlink.
3322- file_id is the inventory ID of the file, if it is to be versioned.
3323- """
3324- trans_id = self._new_entry(name, parent_id, file_id)
3325- self.create_symlink(target, trans_id)
3326- return trans_id
3327-
3328- def new_orphan(self, trans_id, parent_id):
3329- """Schedule an item to be orphaned.
3330-
3331- When a directory is about to be removed, its children, if they are not
3332- versioned are moved out of the way: they don't have a parent anymore.
3333-
3334- :param trans_id: The trans_id of the existing item.
3335- :param parent_id: The parent trans_id of the item.
3336- """
3337- raise NotImplementedError(self.new_orphan)
3338-
3339- def _get_potential_orphans(self, dir_id):
3340- """Find the potential orphans in a directory.
3341-
3342- A directory can't be safely deleted if there are versioned files in it.
3343- If all the contained files are unversioned then they can be orphaned.
3344-
3345- The 'None' return value means that the directory contains at least one
3346- versioned file and should not be deleted.
3347-
3348- :param dir_id: The directory trans id.
3349-
3350- :return: A list of the orphan trans ids or None if at least one
3351- versioned file is present.
3352- """
3353- orphans = []
3354- # Find the potential orphans, stop if one item should be kept
3355- for child_tid in self.by_parent()[dir_id]:
3356- if child_tid in self._removed_contents:
3357- # The child is removed as part of the transform. Since it was
3358- # versioned before, it's not an orphan
3359- continue
3360- if not self.final_is_versioned(child_tid):
3361- # The child is not versioned
3362- orphans.append(child_tid)
3363- else:
3364- # We have a versioned file here, searching for orphans is
3365- # meaningless.
3366- orphans = None
3367- break
3368- return orphans
3369-
3370- def _affected_ids(self):
3371- """Return the set of transform ids affected by the transform"""
3372- trans_ids = set(self._removed_id)
3373- trans_ids.update(self._new_id)
3374- trans_ids.update(self._removed_contents)
3375- trans_ids.update(self._new_contents)
3376- trans_ids.update(self._new_executability)
3377- trans_ids.update(self._new_name)
3378- trans_ids.update(self._new_parent)
3379- return trans_ids
3380-
3381- def _get_file_id_maps(self):
3382- """Return mapping of file_ids to trans_ids in the to and from states"""
3383- trans_ids = self._affected_ids()
3384- from_trans_ids = {}
3385- to_trans_ids = {}
3386- # Build up two dicts: trans_ids associated with file ids in the
3387- # FROM state, vs the TO state.
3388- for trans_id in trans_ids:
3389- from_file_id = self.tree_file_id(trans_id)
3390- if from_file_id is not None:
3391- from_trans_ids[from_file_id] = trans_id
3392- to_file_id = self.final_file_id(trans_id)
3393- if to_file_id is not None:
3394- to_trans_ids[to_file_id] = trans_id
3395- return from_trans_ids, to_trans_ids
3396-
3397- def _from_file_data(self, from_trans_id, from_versioned, from_path):
3398- """Get data about a file in the from (tree) state
3399-
3400- Return a (name, parent, kind, executable) tuple
3401- """
3402- from_path = self._tree_id_paths.get(from_trans_id)
3403- if from_versioned:
3404- # get data from working tree if versioned
3405- from_entry = next(self._tree.iter_entries_by_dir(
3406- specific_files=[from_path]))[1]
3407- from_name = from_entry.name
3408- from_parent = from_entry.parent_id
3409- else:
3410- from_entry = None
3411- if from_path is None:
3412- # File does not exist in FROM state
3413- from_name = None
3414- from_parent = None
3415- else:
3416- # File exists, but is not versioned. Have to use path-
3417- # splitting stuff
3418- from_name = os.path.basename(from_path)
3419- tree_parent = self.get_tree_parent(from_trans_id)
3420- from_parent = self.tree_file_id(tree_parent)
3421- if from_path is not None:
3422- from_kind, from_executable, from_stats = \
3423- self._tree._comparison_data(from_entry, from_path)
3424- else:
3425- from_kind = None
3426- from_executable = False
3427- return from_name, from_parent, from_kind, from_executable
3428-
3429- def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
3430- """Get data about a file in the to (target) state
3431-
3432- Return a (name, parent, kind, executable) tuple
3433- """
3434- to_name = self.final_name(to_trans_id)
3435- to_kind = self.final_kind(to_trans_id)
3436- to_parent = self.final_file_id(self.final_parent(to_trans_id))
3437- if to_trans_id in self._new_executability:
3438- to_executable = self._new_executability[to_trans_id]
3439- elif to_trans_id == from_trans_id:
3440- to_executable = from_executable
3441- else:
3442- to_executable = False
3443- return to_name, to_parent, to_kind, to_executable
3444-
3445- def iter_changes(self):
3446- """Produce output in the same format as Tree.iter_changes.
3447-
3448- Will produce nonsensical results if invoked while inventory/filesystem
3449- conflicts (as reported by TreeTransform.find_conflicts()) are present.
3450-
3451- This reads the Transform, but only reproduces changes involving a
3452- file_id. Files that are not versioned in either of the FROM or TO
3453- states are not reflected.
3454- """
3455- final_paths = FinalPaths(self)
3456- from_trans_ids, to_trans_ids = self._get_file_id_maps()
3457- results = []
3458- # Now iterate through all active file_ids
3459- for file_id in set(from_trans_ids).union(to_trans_ids):
3460- modified = False
3461- from_trans_id = from_trans_ids.get(file_id)
3462- # find file ids, and determine versioning state
3463- if from_trans_id is None:
3464- from_versioned = False
3465- from_trans_id = to_trans_ids[file_id]
3466- else:
3467- from_versioned = True
3468- to_trans_id = to_trans_ids.get(file_id)
3469- if to_trans_id is None:
3470- to_versioned = False
3471- to_trans_id = from_trans_id
3472- else:
3473- to_versioned = True
3474-
3475- if not from_versioned:
3476- from_path = None
3477- else:
3478- from_path = self._tree_id_paths.get(from_trans_id)
3479- if not to_versioned:
3480- to_path = None
3481- else:
3482- to_path = final_paths.get_path(to_trans_id)
3483-
3484- from_name, from_parent, from_kind, from_executable = \
3485- self._from_file_data(from_trans_id, from_versioned, from_path)
3486-
3487- to_name, to_parent, to_kind, to_executable = \
3488- self._to_file_data(to_trans_id, from_trans_id, from_executable)
3489-
3490- if from_kind != to_kind:
3491- modified = True
3492- elif to_kind in ('file', 'symlink') and (
3493- to_trans_id != from_trans_id
3494- or to_trans_id in self._new_contents):
3495- modified = True
3496- if (not modified and from_versioned == to_versioned
3497- and from_parent == to_parent and from_name == to_name
3498- and from_executable == to_executable):
3499- continue
3500- results.append(
3501- TreeChange(
3502- file_id, (from_path, to_path), modified,
3503- (from_versioned, to_versioned),
3504- (from_parent, to_parent),
3505- (from_name, to_name),
3506- (from_kind, to_kind),
3507- (from_executable, to_executable)))
3508-
3509- def path_key(c):
3510- return (c.path[0] or '', c.path[1] or '')
3511- return iter(sorted(results, key=path_key))
3512-
3513- def get_preview_tree(self):
3514- """Return a tree representing the result of the transform.
3515-
3516- The tree is a snapshot, and altering the TreeTransform will invalidate
3517- it.
3518- """
3519- raise NotImplementedError(self.get_preview)
3520-
3521- def commit(self, branch, message, merge_parents=None, strict=False,
3522- timestamp=None, timezone=None, committer=None, authors=None,
3523- revprops=None, revision_id=None):
3524- """Commit the result of this TreeTransform to a branch.
3525-
3526- :param branch: The branch to commit to.
3527- :param message: The message to attach to the commit.
3528- :param merge_parents: Additional parent revision-ids specified by
3529- pending merges.
3530- :param strict: If True, abort the commit if there are unversioned
3531- files.
3532- :param timestamp: if not None, seconds-since-epoch for the time and
3533- date. (May be a float.)
3534- :param timezone: Optional timezone for timestamp, as an offset in
3535- seconds.
3536- :param committer: Optional committer in email-id format.
3537- (e.g. "J Random Hacker <jrandom@example.com>")
3538- :param authors: Optional list of authors in email-id format.
3539- :param revprops: Optional dictionary of revision properties.
3540- :param revision_id: Optional revision id. (Specifying a revision-id
3541- may reduce performance for some non-native formats.)
3542- :return: The revision_id of the revision committed.
3543- """
3544- self._check_malformed()
3545- if strict:
3546- unversioned = set(self._new_contents).difference(set(self._new_id))
3547- for trans_id in unversioned:
3548- if not self.final_is_versioned(trans_id):
3549- raise errors.StrictCommitFailed()
3550-
3551- revno, last_rev_id = branch.last_revision_info()
3552- if last_rev_id == _mod_revision.NULL_REVISION:
3553- if merge_parents is not None:
3554- raise ValueError('Cannot supply merge parents for first'
3555- ' commit.')
3556- parent_ids = []
3557- else:
3558- parent_ids = [last_rev_id]
3559- if merge_parents is not None:
3560- parent_ids.extend(merge_parents)
3561- if self._tree.get_revision_id() != last_rev_id:
3562- raise ValueError('TreeTransform not based on branch basis: %s' %
3563- self._tree.get_revision_id().decode('utf-8'))
3564- from . import commit
3565- revprops = commit.Commit.update_revprops(revprops, branch, authors)
3566- builder = branch.get_commit_builder(parent_ids,
3567- timestamp=timestamp,
3568- timezone=timezone,
3569- committer=committer,
3570- revprops=revprops,
3571- revision_id=revision_id)
3572- preview = self.get_preview_tree()
3573- list(builder.record_iter_changes(preview, last_rev_id,
3574- self.iter_changes()))
3575- builder.finish_inventory()
3576- revision_id = builder.commit(message)
3577- branch.set_last_revision_info(revno + 1, revision_id)
3578- return revision_id
3579-
3580- def _text_parent(self, trans_id):
3581- path = self.tree_path(trans_id)
3582- try:
3583- if path is None or self._tree.kind(path) != 'file':
3584- return None
3585- except errors.NoSuchFile:
3586- return None
3587- return path
3588-
3589- def _get_parents_texts(self, trans_id):
3590- """Get texts for compression parents of this file."""
3591- path = self._text_parent(trans_id)
3592- if path is None:
3593- return ()
3594- return (self._tree.get_file_text(path),)
3595-
3596- def _get_parents_lines(self, trans_id):
3597- """Get lines for compression parents of this file."""
3598- path = self._text_parent(trans_id)
3599- if path is None:
3600- return ()
3601- return (self._tree.get_file_lines(path),)
3602-
3603- def serialize(self, serializer):
3604- """Serialize this TreeTransform.
3605-
3606- :param serializer: A Serialiser like pack.ContainerSerializer.
3607- """
3608- from . import bencode
3609- new_name = {k.encode('utf-8'): v.encode('utf-8')
3610- for k, v in viewitems(self._new_name)}
3611- new_parent = {k.encode('utf-8'): v.encode('utf-8')
3612- for k, v in viewitems(self._new_parent)}
3613- new_id = {k.encode('utf-8'): v
3614- for k, v in viewitems(self._new_id)}
3615- new_executability = {k.encode('utf-8'): int(v)
3616- for k, v in viewitems(self._new_executability)}
3617- tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
3618- for k, v in viewitems(self._tree_path_ids)}
3619- non_present_ids = {k: v.encode('utf-8')
3620- for k, v in viewitems(self._non_present_ids)}
3621- removed_contents = [trans_id.encode('utf-8')
3622- for trans_id in self._removed_contents]
3623- removed_id = [trans_id.encode('utf-8')
3624- for trans_id in self._removed_id]
3625- attribs = {
3626- b'_id_number': self._id_number,
3627- b'_new_name': new_name,
3628- b'_new_parent': new_parent,
3629- b'_new_executability': new_executability,
3630- b'_new_id': new_id,
3631- b'_tree_path_ids': tree_path_ids,
3632- b'_removed_id': removed_id,
3633- b'_removed_contents': removed_contents,
3634- b'_non_present_ids': non_present_ids,
3635- }
3636- yield serializer.bytes_record(bencode.bencode(attribs),
3637- ((b'attribs',),))
3638- for trans_id, kind in sorted(viewitems(self._new_contents)):
3639- if kind == 'file':
3640- with open(self._limbo_name(trans_id), 'rb') as cur_file:
3641- lines = cur_file.readlines()
3642- parents = self._get_parents_lines(trans_id)
3643- mpdiff = multiparent.MultiParent.from_lines(lines, parents)
3644- content = b''.join(mpdiff.to_patch())
3645- if kind == 'directory':
3646- content = b''
3647- if kind == 'symlink':
3648- content = self._read_symlink_target(trans_id)
3649- if not isinstance(content, bytes):
3650- content = content.encode('utf-8')
3651- yield serializer.bytes_record(
3652- content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
3653-
3654- def deserialize(self, records):
3655- """Deserialize a stored TreeTransform.
3656-
3657- :param records: An iterable of (names, content) tuples, as per
3658- pack.ContainerPushParser.
3659- """
3660- from . import bencode
3661- names, content = next(records)
3662- attribs = bencode.bdecode(content)
3663- self._id_number = attribs[b'_id_number']
3664- self._new_name = {k.decode('utf-8'): v.decode('utf-8')
3665- for k, v in viewitems(attribs[b'_new_name'])}
3666- self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
3667- for k, v in viewitems(attribs[b'_new_parent'])}
3668- self._new_executability = {
3669- k.decode('utf-8'): bool(v)
3670- for k, v in viewitems(attribs[b'_new_executability'])}
3671- self._new_id = {k.decode('utf-8'): v
3672- for k, v in viewitems(attribs[b'_new_id'])}
3673- self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
3674- self._tree_path_ids = {}
3675- self._tree_id_paths = {}
3676- for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
3677- path = bytepath.decode('utf-8')
3678- trans_id = trans_id.decode('utf-8')
3679- self._tree_path_ids[path] = trans_id
3680- self._tree_id_paths[trans_id] = path
3681- self._removed_id = {trans_id.decode('utf-8')
3682- for trans_id in attribs[b'_removed_id']}
3683- self._removed_contents = set(
3684- trans_id.decode('utf-8')
3685- for trans_id in attribs[b'_removed_contents'])
3686- self._non_present_ids = {
3687- k: v.decode('utf-8')
3688- for k, v in viewitems(attribs[b'_non_present_ids'])}
3689- for ((trans_id, kind),), content in records:
3690- trans_id = trans_id.decode('utf-8')
3691- kind = kind.decode('ascii')
3692- if kind == 'file':
3693- mpdiff = multiparent.MultiParent.from_patch(content)
3694- lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
3695- self.create_file(lines, trans_id)
3696- if kind == 'directory':
3697- self.create_directory(trans_id)
3698- if kind == 'symlink':
3699- self.create_symlink(content.decode('utf-8'), trans_id)
3700-
3701- def create_file(self, contents, trans_id, mode_id=None, sha1=None):
3702- """Schedule creation of a new file.
3703-
3704- :seealso: new_file.
3705-
3706- :param contents: an iterator of strings, all of which will be written
3707- to the target destination.
3708- :param trans_id: TreeTransform handle
3709- :param mode_id: If not None, force the mode of the target file to match
3710- the mode of the object referenced by mode_id.
3711- Otherwise, we will try to preserve mode bits of an existing file.
3712- :param sha1: If the sha1 of this content is already known, pass it in.
3713- We can use it to prevent future sha1 computations.
3714- """
3715- raise NotImplementedError(self.create_file)
3716-
3717- def create_directory(self, trans_id):
3718- """Schedule creation of a new directory.
3719-
3720- See also new_directory.
3721- """
3722- raise NotImplementedError(self.create_directory)
3723-
3724- def create_symlink(self, target, trans_id):
3725- """Schedule creation of a new symbolic link.
3726-
3727- target is a bytestring.
3728- See also new_symlink.
3729- """
3730- raise NotImplementedError(self.create_symlink)
3731-
3732- def create_hardlink(self, path, trans_id):
3733- """Schedule creation of a hard link"""
3734- raise NotImplementedError(self.create_hardlink)
3735-
3736- def cancel_creation(self, trans_id):
3737- """Cancel the creation of new file contents."""
3738- raise NotImplementedError(self.cancel_creation)
3739-
3740- def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
3741- """Apply all changes to the inventory and filesystem.
3742-
3743- If filesystem or inventory conflicts are present, MalformedTransform
3744- will be thrown.
3745-
3746- If apply succeeds, finalize is not necessary.
3747-
3748- :param no_conflicts: if True, the caller guarantees there are no
3749- conflicts, so no check is made.
3750- :param precomputed_delta: An inventory delta to use instead of
3751- calculating one.
3752- :param _mover: Supply an alternate FileMover, for testing
3753- """
3754- raise NotImplementedError(self.apply)
3755-
3756-
3757-class DiskTreeTransform(TreeTransformBase):
3758- """Tree transform storing its contents on disk."""
3759-
3760- def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
3761- """Constructor.
3762- :param tree: The tree that will be transformed, but not necessarily
3763- the output tree.
3764- :param limbodir: A directory where new files can be stored until
3765- they are installed in their proper places
3766- :param pb: ignored
3767- :param case_sensitive: If True, the target of the transform is
3768- case sensitive, not just case preserving.
3769- """
3770- TreeTransformBase.__init__(self, tree, pb, case_sensitive)
3771- self._limbodir = limbodir
3772- self._deletiondir = None
3773- # A mapping of transform ids to their limbo filename
3774- self._limbo_files = {}
3775- self._possibly_stale_limbo_files = set()
3776- # A mapping of transform ids to a set of the transform ids of children
3777- # that their limbo directory has
3778- self._limbo_children = {}
3779- # Map transform ids to maps of child filename to child transform id
3780- self._limbo_children_names = {}
3781- # List of transform ids that need to be renamed from limbo into place
3782- self._needs_rename = set()
3783- self._creation_mtime = None
3784- self._create_symlinks = osutils.supports_symlinks(self._limbodir)
3785-
3786- def finalize(self):
3787- """Release the working tree lock, if held, clean up limbo dir.
3788-
3789- This is required if apply has not been invoked, but can be invoked
3790- even after apply.
3791- """
3792- if self._tree is None:
3793- return
3794- try:
3795- limbo_paths = list(viewvalues(self._limbo_files))
3796- limbo_paths.extend(self._possibly_stale_limbo_files)
3797- limbo_paths.sort(reverse=True)
3798- for path in limbo_paths:
3799- try:
3800- delete_any(path)
3801- except OSError as e:
3802- if e.errno != errno.ENOENT:
3803- raise
3804- # XXX: warn? perhaps we just got interrupted at an
3805- # inconvenient moment, but perhaps files are disappearing
3806- # from under us?
3807- try:
3808- delete_any(self._limbodir)
3809- except OSError:
3810- # We don't especially care *why* the dir is immortal.
3811- raise ImmortalLimbo(self._limbodir)
3812- try:
3813- if self._deletiondir is not None:
3814- delete_any(self._deletiondir)
3815- except OSError:
3816- raise errors.ImmortalPendingDeletion(self._deletiondir)
3817- finally:
3818- TreeTransformBase.finalize(self)
3819-
3820- def _limbo_supports_executable(self):
3821- """Check if the limbo path supports the executable bit."""
3822- return osutils.supports_executable(self._limbodir)
3823-
3824- def _limbo_name(self, trans_id):
3825- """Generate the limbo name of a file"""
3826- limbo_name = self._limbo_files.get(trans_id)
3827- if limbo_name is None:
3828- limbo_name = self._generate_limbo_path(trans_id)
3829- self._limbo_files[trans_id] = limbo_name
3830- return limbo_name
3831-
3832- def _generate_limbo_path(self, trans_id):
3833- """Generate a limbo path using the trans_id as the relative path.
3834-
3835- This is suitable as a fallback, and when the transform should not be
3836- sensitive to the path encoding of the limbo directory.
3837- """
3838- self._needs_rename.add(trans_id)
3839- return pathjoin(self._limbodir, trans_id)
3840-
3841- def adjust_path(self, name, parent, trans_id):
3842- previous_parent = self._new_parent.get(trans_id)
3843- previous_name = self._new_name.get(trans_id)
3844- super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
3845- if (trans_id in self._limbo_files
3846- and trans_id not in self._needs_rename):
3847- self._rename_in_limbo([trans_id])
3848- if previous_parent != parent:
3849- self._limbo_children[previous_parent].remove(trans_id)
3850- if previous_parent != parent or previous_name != name:
3851- del self._limbo_children_names[previous_parent][previous_name]
3852-
3853- def _rename_in_limbo(self, trans_ids):
3854- """Fix limbo names so that the right final path is produced.
3855-
3856- This means we outsmarted ourselves-- we tried to avoid renaming
3857- these files later by creating them with their final names in their
3858- final parents. But now the previous name or parent is no longer
3859- suitable, so we have to rename them.
3860-
3861- Even for trans_ids that have no new contents, we must remove their
3862- entries from _limbo_files, because they are now stale.
3863- """
3864- for trans_id in trans_ids:
3865- old_path = self._limbo_files[trans_id]
3866- self._possibly_stale_limbo_files.add(old_path)
3867- del self._limbo_files[trans_id]
3868- if trans_id not in self._new_contents:
3869- continue
3870- new_path = self._limbo_name(trans_id)
3871- os.rename(old_path, new_path)
3872- self._possibly_stale_limbo_files.remove(old_path)
3873- for descendant in self._limbo_descendants(trans_id):
3874- desc_path = self._limbo_files[descendant]
3875- desc_path = new_path + desc_path[len(old_path):]
3876- self._limbo_files[descendant] = desc_path
3877-
3878- def _limbo_descendants(self, trans_id):
3879- """Return the set of trans_ids whose limbo paths descend from this."""
3880- descendants = set(self._limbo_children.get(trans_id, []))
3881- for descendant in list(descendants):
3882- descendants.update(self._limbo_descendants(descendant))
3883- return descendants
3884-
3885- def _set_mode(self, trans_id, mode_id, typefunc):
3886- raise NotImplementedError(self._set_mode)
3887-
3888- def create_file(self, contents, trans_id, mode_id=None, sha1=None):
3889- """Schedule creation of a new file.
3890-
3891- :seealso: new_file.
3892-
3893- :param contents: an iterator of strings, all of which will be written
3894- to the target destination.
3895- :param trans_id: TreeTransform handle
3896- :param mode_id: If not None, force the mode of the target file to match
3897- the mode of the object referenced by mode_id.
3898- Otherwise, we will try to preserve mode bits of an existing file.
3899- :param sha1: If the sha1 of this content is already known, pass it in.
3900- We can use it to prevent future sha1 computations.
3901- """
3902- name = self._limbo_name(trans_id)
3903- with open(name, 'wb') as f:
3904- unique_add(self._new_contents, trans_id, 'file')
3905- f.writelines(contents)
3906- self._set_mtime(name)
3907- self._set_mode(trans_id, mode_id, S_ISREG)
3908- # It is unfortunate we have to use lstat instead of fstat, but we just
3909- # used utime and chmod on the file, so we need the accurate final
3910- # details.
3911- if sha1 is not None:
3912- self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
3913-
3914- def _read_symlink_target(self, trans_id):
3915- return os.readlink(self._limbo_name(trans_id))
3916-
3917- def _set_mtime(self, path):
3918- """All files that are created get the same mtime.
3919-
3920- This time is set by the first object to be created.
3921- """
3922- if self._creation_mtime is None:
3923- self._creation_mtime = time.time()
3924- os.utime(path, (self._creation_mtime, self._creation_mtime))
3925-
3926- def create_hardlink(self, path, trans_id):
3927- """Schedule creation of a hard link"""
3928- name = self._limbo_name(trans_id)
3929- try:
3930- os.link(path, name)
3931- except OSError as e:
3932- if e.errno != errno.EPERM:
3933- raise
3934- raise errors.HardLinkNotSupported(path)
3935- try:
3936- unique_add(self._new_contents, trans_id, 'file')
3937- except BaseException:
3938- # Clean up the file, it never got registered so
3939- # TreeTransform.finalize() won't clean it up.
3940- os.unlink(name)
3941- raise
3942-
3943- def create_directory(self, trans_id):
3944- """Schedule creation of a new directory.
3945-
3946- See also new_directory.
3947- """
3948- os.mkdir(self._limbo_name(trans_id))
3949- unique_add(self._new_contents, trans_id, 'directory')
3950-
3951- def create_symlink(self, target, trans_id):
3952- """Schedule creation of a new symbolic link.
3953-
3954- target is a bytestring.
3955- See also new_symlink.
3956- """
3957- if self._create_symlinks:
3958- os.symlink(target, self._limbo_name(trans_id))
3959- else:
3960- try:
3961- path = FinalPaths(self).get_path(trans_id)
3962- except KeyError:
3963- path = None
3964- trace.warning(
3965- 'Unable to create symlink "%s" on this filesystem.' % (path,))
3966- # We add symlink to _new_contents even if they are unsupported
3967- # and not created. These entries are subsequently used to avoid
3968- # conflicts on platforms that don't support symlink
3969- unique_add(self._new_contents, trans_id, 'symlink')
3970-
3971- def cancel_creation(self, trans_id):
3972- """Cancel the creation of new file contents."""
3973- del self._new_contents[trans_id]
3974- if trans_id in self._observed_sha1s:
3975- del self._observed_sha1s[trans_id]
3976- children = self._limbo_children.get(trans_id)
3977- # if this is a limbo directory with children, move them before removing
3978- # the directory
3979- if children is not None:
3980- self._rename_in_limbo(children)
3981- del self._limbo_children[trans_id]
3982- del self._limbo_children_names[trans_id]
3983- delete_any(self._limbo_name(trans_id))
3984-
3985- def new_orphan(self, trans_id, parent_id):
3986- conf = self._tree.get_config_stack()
3987- handle_orphan = conf.get('transform.orphan_policy')
3988- handle_orphan(self, trans_id, parent_id)
3989-
3990-
3991 class OrphaningError(errors.BzrError):
3992
3993 # Only bugs could lead to such exception being seen by the user

Subscribers

People subscribed via source and target branches