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 |
Related bugs: |
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 |