Merge lp:~jelmer/brz/merge-3.1 into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/merge-3.1
Merge into: lp:brz
Diff against target: 10358 lines (+3312/-2582)
90 files modified
breezy/builtins.py (+1/-2)
breezy/bzr/_dirstate_helpers_pyx.pyx (+8/-8)
breezy/bzr/conflicts.py (+663/-0)
breezy/bzr/dirstate.py (+8/-8)
breezy/bzr/inventorytree.py (+116/-19)
breezy/bzr/tests/__init__.py (+1/-0)
breezy/bzr/tests/test_conflicts.py (+133/-0)
breezy/bzr/transform.py (+129/-22)
breezy/bzr/vf_repository.py (+2/-2)
breezy/bzr/workingtree.py (+65/-20)
breezy/bzr/workingtree_4.py (+13/-8)
breezy/commit.py (+0/-1)
breezy/conflicts.py (+29/-598)
breezy/delta.py (+8/-10)
breezy/errors.py (+5/-5)
breezy/git/commit.py (+26/-10)
breezy/git/mapping.py (+1/-1)
breezy/git/remote.py (+12/-4)
breezy/git/tests/test_remote.py (+8/-0)
breezy/git/tests/test_tree.py (+1/-1)
breezy/git/tests/test_workingtree.py (+21/-3)
breezy/git/transform.py (+560/-442)
breezy/git/tree.py (+206/-316)
breezy/git/workingtree.py (+115/-38)
breezy/help_topics/en/configuration.txt (+38/-38)
breezy/help_topics/en/conflict-types.txt (+1/-1)
breezy/help_topics/en/glossary.txt (+10/-10)
breezy/merge.py (+36/-91)
breezy/mutabletree.py (+2/-37)
breezy/plugins/bash_completion/README.txt (+17/-29)
breezy/plugins/darcs/__init__.py (+1/-2)
breezy/plugins/fastimport/doc/notes.txt (+1/-1)
breezy/plugins/fastimport/revision_store.py (+5/-5)
breezy/plugins/fossil/__init__.py (+1/-2)
breezy/plugins/github/hoster.py (+12/-4)
breezy/plugins/gitlab/hoster.py (+36/-10)
breezy/plugins/hg/__init__.py (+4/-2)
breezy/plugins/launchpad/hoster.py (+3/-2)
breezy/plugins/mtn/__init__.py (+2/-2)
breezy/plugins/propose/cmds.py (+26/-18)
breezy/plugins/rewrite/rebase.py (+2/-2)
breezy/plugins/rewrite/tests/test_rebase.py (+1/-1)
breezy/plugins/svn/__init__.py (+1/-2)
breezy/plugins/weave_fmt/test_workingtree.py (+7/-4)
breezy/plugins/weave_fmt/workingtree.py (+3/-2)
breezy/propose.py (+12/-0)
breezy/tests/blackbox/test_merge.py (+7/-6)
breezy/tests/blackbox/test_resolve.py (+3/-2)
breezy/tests/blackbox/test_status.py (+4/-7)
breezy/tests/features.py (+2/-0)
breezy/tests/per_intertree/test_compare.py (+20/-18)
breezy/tests/per_merger.py (+1/-1)
breezy/tests/per_repository/test_check.py (+2/-2)
breezy/tests/per_repository/test_commit_builder.py (+5/-4)
breezy/tests/per_tree/test_transform.py (+48/-23)
breezy/tests/per_tree/test_walkdirs.py (+34/-43)
breezy/tests/per_workingtree/test_commit.py (+6/-3)
breezy/tests/per_workingtree/test_merge_from_branch.py (+6/-3)
breezy/tests/per_workingtree/test_smart_add.py (+3/-0)
breezy/tests/per_workingtree/test_transform.py (+358/-213)
breezy/tests/per_workingtree/test_unversion.py (+4/-1)
breezy/tests/per_workingtree/test_walkdirs.py (+31/-67)
breezy/tests/per_workingtree/test_workingtree.py (+20/-12)
breezy/tests/test_commit.py (+5/-5)
breezy/tests/test_conflicts.py (+38/-66)
breezy/tests/test_delta.py (+5/-5)
breezy/tests/test_merge.py (+55/-32)
breezy/tests/test_merge_core.py (+1/-1)
breezy/tests/test_transform.py (+2/-3)
breezy/tests/test_workingtree.py (+13/-12)
breezy/tests/test_workspace.py (+6/-0)
breezy/transform.py (+166/-157)
breezy/transport/http/__init__.py (+6/-4)
breezy/tree.py (+17/-24)
breezy/upstream_import.py (+2/-2)
breezy/workingtree.py (+11/-7)
breezy/workspace.py (+7/-7)
doc/developers/HACKING.txt (+2/-2)
doc/developers/win32_build_setup.txt (+5/-5)
doc/en/admin-guide/advanced.txt (+20/-20)
doc/en/admin-guide/backup.txt (+7/-7)
doc/en/admin-guide/hooks-plugins.txt (+5/-5)
doc/en/admin-guide/migration.txt (+7/-11)
doc/en/admin-guide/simple-setups.txt (+9/-9)
doc/en/release-notes/brz-3.1.txt (+7/-0)
doc/en/upgrade-guide/overview.txt (+1/-1)
doc/en/user-guide/configuring_breezy.txt (+2/-2)
doc/en/user-guide/plugins.txt (+4/-4)
doc/en/user-guide/recording_changes.txt (+1/-1)
setup.py (+2/-2)
To merge this branch: bzr merge lp:~jelmer/brz/merge-3.1
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+389690@code.launchpad.net

Commit message

Merge lp:brz/3.1.

Description of the change

Merge lp:brz/3.1.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/builtins.py'
2--- breezy/builtins.py 2020-07-18 23:14:00 +0000
3+++ breezy/builtins.py 2020-08-23 01:05:31 +0000
4@@ -61,7 +61,6 @@
5 views,
6 )
7 from breezy.branch import Branch
8-from breezy.conflicts import ConflictList
9 from breezy.transport import memory
10 from breezy.smtp_connection import SMTPConnection
11 from breezy.workingtree import WorkingTree
12@@ -4820,7 +4819,7 @@
13 restore_files = [c.path for c in conflicts
14 if c.typestring in allowed_conflicts]
15 _mod_merge.transform_tree(tree, tree.basis_tree(), interesting_files)
16- tree.set_conflicts(ConflictList(new_conflicts))
17+ tree.set_conflicts(new_conflicts)
18 if file_list is not None:
19 restore_files = file_list
20 for filename in restore_files:
21
22=== modified file 'breezy/bzr/_dirstate_helpers_pyx.pyx'
23--- breezy/bzr/_dirstate_helpers_pyx.pyx 2020-06-08 19:42:53 +0000
24+++ breezy/bzr/_dirstate_helpers_pyx.pyx 2020-08-23 01:05:31 +0000
25@@ -31,7 +31,7 @@
26 from .. import cache_utf8, errors, osutils
27 from .dirstate import DirState, DirstateCorrupt
28 from ..osutils import parent_directories, pathjoin, splitpath, is_inside_any, is_inside
29-from ..tree import TreeChange
30+from .inventorytree import InventoryTreeChange
31
32
33 # This is the Windows equivalent of ENOTDIR
34@@ -1303,7 +1303,7 @@
35 else:
36 path_u = self.utf8_decode(path)[0]
37 source_kind = _minikind_to_kind(source_minikind)
38- return TreeChange(entry[0][2],
39+ return InventoryTreeChange(entry[0][2],
40 (old_path_u, path_u),
41 content_change,
42 (True, True),
43@@ -1336,7 +1336,7 @@
44 and S_IXUSR & path_info[3].st_mode)
45 else:
46 target_exec = target_details[3]
47- return TreeChange(entry[0][2],
48+ return InventoryTreeChange(entry[0][2],
49 (None, self.utf8_decode(path)[0]),
50 True,
51 (False, True),
52@@ -1346,7 +1346,7 @@
53 (None, target_exec)), True
54 else:
55 # Its a missing file, report it as such.
56- return TreeChange(entry[0][2],
57+ return InventoryTreeChange(entry[0][2],
58 (None, self.utf8_decode(path)[0]),
59 False,
60 (False, True),
61@@ -1364,7 +1364,7 @@
62 parent_id = self.state._get_entry(self.source_index, path_utf8=entry[0][0])[0][2]
63 if parent_id == entry[0][2]:
64 parent_id = None
65- return TreeChange(
66+ return InventoryTreeChange(
67 entry[0][2],
68 (self.utf8_decode(old_path)[0], None),
69 True,
70@@ -1562,7 +1562,7 @@
71 new_executable = bool(
72 stat.S_ISREG(self.root_dir_info[3].st_mode)
73 and stat.S_IEXEC & self.root_dir_info[3].st_mode)
74- return TreeChange(
75+ return InventoryTreeChange(
76 None,
77 (None, self.current_root_unicode),
78 True,
79@@ -1674,7 +1674,7 @@
80 new_executable = bool(
81 stat.S_ISREG(current_path_info[3].st_mode)
82 and stat.S_IEXEC & current_path_info[3].st_mode)
83- return TreeChange(
84+ return InventoryTreeChange(
85 None,
86 (None, self.utf8_decode(current_path_info[0])[0]),
87 True,
88@@ -1844,7 +1844,7 @@
89 if changed is not None:
90 raise AssertionError(
91 "result is not None: %r" % result)
92- result = TreeChange(
93+ result = InventoryTreeChange(
94 None,
95 (None, relpath_unicode),
96 True,
97
98=== added file 'breezy/bzr/conflicts.py'
99--- breezy/bzr/conflicts.py 1970-01-01 00:00:00 +0000
100+++ breezy/bzr/conflicts.py 2020-08-23 01:05:31 +0000
101@@ -0,0 +1,663 @@
102+# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 Canonical Ltd
103+#
104+# This program is free software; you can redistribute it and/or modify
105+# it under the terms of the GNU General Public License as published by
106+# the Free Software Foundation; either version 2 of the License, or
107+# (at your option) any later version.
108+#
109+# This program is distributed in the hope that it will be useful,
110+# but WITHOUT ANY WARRANTY; without even the implied warranty of
111+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
112+# GNU General Public License for more details.
113+#
114+# You should have received a copy of the GNU General Public License
115+# along with this program; if not, write to the Free Software
116+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
117+
118+from __future__ import absolute_import
119+
120+import errno
121+import os
122+import re
123+
124+from ..lazy_import import lazy_import
125+lazy_import(globals(), """
126+
127+from breezy import (
128+ cache_utf8,
129+ errors,
130+ rio,
131+ transform,
132+ osutils,
133+ )
134+""")
135+
136+from ..conflicts import (
137+ Conflict as BaseConflict,
138+ ConflictList as BaseConflictList,
139+ )
140+
141+
142+CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
143+
144+
145+class Conflict(BaseConflict):
146+ """Base class for all types of conflict"""
147+
148+ # FIXME: cleanup should take care of that ? -- vila 091229
149+ has_files = False
150+
151+ def __init__(self, path, file_id=None):
152+ super(Conflict, self).__init__(path)
153+ # the factory blindly transfers the Stanza values to __init__ and
154+ # Stanza is purely a Unicode api.
155+ if isinstance(file_id, str):
156+ file_id = cache_utf8.encode(file_id)
157+ self.file_id = file_id
158+
159+ def as_stanza(self):
160+ s = rio.Stanza(type=self.typestring, path=self.path)
161+ if self.file_id is not None:
162+ # Stanza requires Unicode apis
163+ s.add('file_id', self.file_id.decode('utf8'))
164+ return s
165+
166+ def _cmp_list(self):
167+ return [type(self), self.path, self.file_id]
168+
169+ def __cmp__(self, other):
170+ if getattr(other, "_cmp_list", None) is None:
171+ return -1
172+ x = self._cmp_list()
173+ y = other._cmp_list()
174+ return (x > y) - (x < y)
175+
176+ def __hash__(self):
177+ return hash((type(self), self.path, self.file_id))
178+
179+ def __eq__(self, other):
180+ return self.__cmp__(other) == 0
181+
182+ def __ne__(self, other):
183+ return not self.__eq__(other)
184+
185+ def __unicode__(self):
186+ return self.describe()
187+
188+ def __str__(self):
189+ return self.describe()
190+
191+ def describe(self):
192+ return self.format % self.__dict__
193+
194+ def __repr__(self):
195+ rdict = dict(self.__dict__)
196+ rdict['class'] = self.__class__.__name__
197+ return self.rformat % rdict
198+
199+ @staticmethod
200+ def factory(type, **kwargs):
201+ global ctype
202+ return ctype[type](**kwargs)
203+
204+ @staticmethod
205+ def sort_key(conflict):
206+ if conflict.path is not None:
207+ return conflict.path, conflict.typestring
208+ elif getattr(conflict, "conflict_path", None) is not None:
209+ return conflict.conflict_path, conflict.typestring
210+ else:
211+ return None, conflict.typestring
212+
213+ def do(self, action, tree):
214+ """Apply the specified action to the conflict.
215+
216+ :param action: The method name to call.
217+
218+ :param tree: The tree passed as a parameter to the method.
219+ """
220+ meth = getattr(self, 'action_%s' % action, None)
221+ if meth is None:
222+ raise NotImplementedError(self.__class__.__name__ + '.' + action)
223+ meth(tree)
224+
225+ def action_auto(self, tree):
226+ raise NotImplementedError(self.action_auto)
227+
228+ def action_done(self, tree):
229+ """Mark the conflict as solved once it has been handled."""
230+ # This method does nothing but simplifies the design of upper levels.
231+ pass
232+
233+ def action_take_this(self, tree):
234+ raise NotImplementedError(self.action_take_this)
235+
236+ def action_take_other(self, tree):
237+ raise NotImplementedError(self.action_take_other)
238+
239+ def _resolve_with_cleanups(self, tree, *args, **kwargs):
240+ with tree.transform() as tt:
241+ self._resolve(tt, *args, **kwargs)
242+
243+
244+class ConflictList(BaseConflictList):
245+
246+ @staticmethod
247+ def from_stanzas(stanzas):
248+ """Produce a new ConflictList from an iterable of stanzas"""
249+ conflicts = ConflictList()
250+ for stanza in stanzas:
251+ conflicts.append(Conflict.factory(**stanza.as_dict()))
252+ return conflicts
253+
254+ def to_stanzas(self):
255+ """Generator of stanzas"""
256+ for conflict in self:
257+ yield conflict.as_stanza()
258+
259+ def select_conflicts(self, tree, paths, ignore_misses=False,
260+ recurse=False):
261+ """Select the conflicts associated with paths in a tree.
262+
263+ File-ids are also used for this.
264+ :return: a pair of ConflictLists: (not_selected, selected)
265+ """
266+ path_set = set(paths)
267+ ids = {}
268+ selected_paths = set()
269+ new_conflicts = ConflictList()
270+ selected_conflicts = ConflictList()
271+ for path in paths:
272+ file_id = tree.path2id(path)
273+ if file_id is not None:
274+ ids[file_id] = path
275+
276+ for conflict in self:
277+ selected = False
278+ for key in ('path', 'conflict_path'):
279+ cpath = getattr(conflict, key, None)
280+ if cpath is None:
281+ continue
282+ if cpath in path_set:
283+ selected = True
284+ selected_paths.add(cpath)
285+ if recurse:
286+ if osutils.is_inside_any(path_set, cpath):
287+ selected = True
288+ selected_paths.add(cpath)
289+
290+ for key in ('file_id', 'conflict_file_id'):
291+ cfile_id = getattr(conflict, key, None)
292+ if cfile_id is None:
293+ continue
294+ try:
295+ cpath = ids[cfile_id]
296+ except KeyError:
297+ continue
298+ selected = True
299+ selected_paths.add(cpath)
300+ if selected:
301+ selected_conflicts.append(conflict)
302+ else:
303+ new_conflicts.append(conflict)
304+ if ignore_misses is not True:
305+ for path in [p for p in paths if p not in selected_paths]:
306+ if not os.path.exists(tree.abspath(path)):
307+ print("%s does not exist" % path)
308+ else:
309+ print("%s is not conflicted" % path)
310+ return new_conflicts, selected_conflicts
311+
312+
313+
314+
315+class PathConflict(Conflict):
316+ """A conflict was encountered merging file paths"""
317+
318+ typestring = 'path conflict'
319+
320+ format = 'Path conflict: %(path)s / %(conflict_path)s'
321+
322+ rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
323+
324+ def __init__(self, path, conflict_path=None, file_id=None):
325+ Conflict.__init__(self, path, file_id)
326+ self.conflict_path = conflict_path
327+
328+ def as_stanza(self):
329+ s = Conflict.as_stanza(self)
330+ if self.conflict_path is not None:
331+ s.add('conflict_path', self.conflict_path)
332+ return s
333+
334+ def associated_filenames(self):
335+ # No additional files have been generated here
336+ return []
337+
338+ def _resolve(self, tt, file_id, path, winner):
339+ """Resolve the conflict.
340+
341+ :param tt: The TreeTransform where the conflict is resolved.
342+ :param file_id: The retained file id.
343+ :param path: The retained path.
344+ :param winner: 'this' or 'other' indicates which side is the winner.
345+ """
346+ path_to_create = None
347+ if winner == 'this':
348+ if self.path == '<deleted>':
349+ return # Nothing to do
350+ if self.conflict_path == '<deleted>':
351+ path_to_create = self.path
352+ revid = tt._tree.get_parent_ids()[0]
353+ elif winner == 'other':
354+ if self.conflict_path == '<deleted>':
355+ return # Nothing to do
356+ if self.path == '<deleted>':
357+ path_to_create = self.conflict_path
358+ # FIXME: If there are more than two parents we may need to
359+ # iterate. Taking the last parent is the safer bet in the mean
360+ # time. -- vila 20100309
361+ revid = tt._tree.get_parent_ids()[-1]
362+ else:
363+ # Programmer error
364+ raise AssertionError('bad winner: %r' % (winner,))
365+ if path_to_create is not None:
366+ tid = tt.trans_id_tree_path(path_to_create)
367+ tree = self._revision_tree(tt._tree, revid)
368+ transform.create_from_tree(
369+ tt, tid, tree, tree.id2path(file_id))
370+ tt.version_file(tid, file_id=file_id)
371+ else:
372+ tid = tt.trans_id_file_id(file_id)
373+ # Adjust the path for the retained file id
374+ parent_tid = tt.get_tree_parent(tid)
375+ tt.adjust_path(osutils.basename(path), parent_tid, tid)
376+ tt.apply()
377+
378+ def _revision_tree(self, tree, revid):
379+ return tree.branch.repository.revision_tree(revid)
380+
381+ def _infer_file_id(self, tree):
382+ # Prior to bug #531967, file_id wasn't always set, there may still be
383+ # conflict files in the wild so we need to cope with them
384+ # Establish which path we should use to find back the file-id
385+ possible_paths = []
386+ for p in (self.path, self.conflict_path):
387+ if p == '<deleted>':
388+ # special hard-coded path
389+ continue
390+ if p is not None:
391+ possible_paths.append(p)
392+ # Search the file-id in the parents with any path available
393+ file_id = None
394+ for revid in tree.get_parent_ids():
395+ revtree = self._revision_tree(tree, revid)
396+ for p in possible_paths:
397+ file_id = revtree.path2id(p)
398+ if file_id is not None:
399+ return revtree, file_id
400+ return None, None
401+
402+ def action_take_this(self, tree):
403+ if self.file_id is not None:
404+ self._resolve_with_cleanups(tree, self.file_id, self.path,
405+ winner='this')
406+ else:
407+ # Prior to bug #531967 we need to find back the file_id and restore
408+ # the content from there
409+ revtree, file_id = self._infer_file_id(tree)
410+ tree.revert([revtree.id2path(file_id)],
411+ old_tree=revtree, backups=False)
412+
413+ def action_take_other(self, tree):
414+ if self.file_id is not None:
415+ self._resolve_with_cleanups(tree, self.file_id,
416+ self.conflict_path,
417+ winner='other')
418+ else:
419+ # Prior to bug #531967 we need to find back the file_id and restore
420+ # the content from there
421+ revtree, file_id = self._infer_file_id(tree)
422+ tree.revert([revtree.id2path(file_id)],
423+ old_tree=revtree, backups=False)
424+
425+
426+class ContentsConflict(PathConflict):
427+ """The files are of different types (or both binary), or not present"""
428+
429+ has_files = True
430+
431+ typestring = 'contents conflict'
432+
433+ format = 'Contents conflict in %(path)s'
434+
435+ def associated_filenames(self):
436+ return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
437+
438+ def _resolve(self, tt, suffix_to_remove):
439+ """Resolve the conflict.
440+
441+ :param tt: The TreeTransform where the conflict is resolved.
442+ :param suffix_to_remove: Either 'THIS' or 'OTHER'
443+
444+ The resolution is symmetric: when taking THIS, OTHER is deleted and
445+ item.THIS is renamed into item and vice-versa.
446+ """
447+ try:
448+ # Delete 'item.THIS' or 'item.OTHER' depending on
449+ # suffix_to_remove
450+ tt.delete_contents(
451+ tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
452+ except errors.NoSuchFile:
453+ # There are valid cases where 'item.suffix_to_remove' either
454+ # never existed or was already deleted (including the case
455+ # where the user deleted it)
456+ pass
457+ try:
458+ this_path = tt._tree.id2path(self.file_id)
459+ except errors.NoSuchId:
460+ # The file is not present anymore. This may happen if the user
461+ # deleted the file either manually or when resolving a conflict on
462+ # the parent. We may raise some exception to indicate that the
463+ # conflict doesn't exist anymore and as such doesn't need to be
464+ # resolved ? -- vila 20110615
465+ this_tid = None
466+ else:
467+ this_tid = tt.trans_id_tree_path(this_path)
468+ if this_tid is not None:
469+ # Rename 'item.suffix_to_remove' (note that if
470+ # 'item.suffix_to_remove' has been deleted, this is a no-op)
471+ parent_tid = tt.get_tree_parent(this_tid)
472+ tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
473+ tt.apply()
474+
475+ def action_take_this(self, tree):
476+ self._resolve_with_cleanups(tree, 'OTHER')
477+
478+ def action_take_other(self, tree):
479+ self._resolve_with_cleanups(tree, 'THIS')
480+
481+
482+# TODO: There should be a base revid attribute to better inform the user about
483+# how the conflicts were generated.
484+class TextConflict(Conflict):
485+ """The merge algorithm could not resolve all differences encountered."""
486+
487+ has_files = True
488+
489+ typestring = 'text conflict'
490+
491+ format = 'Text conflict in %(path)s'
492+
493+ rformat = '%(class)s(%(path)r, %(file_id)r)'
494+
495+ _conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
496+
497+ def associated_filenames(self):
498+ return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
499+
500+ def _resolve(self, tt, winner_suffix):
501+ """Resolve the conflict by copying one of .THIS or .OTHER into file.
502+
503+ :param tt: The TreeTransform where the conflict is resolved.
504+ :param winner_suffix: Either 'THIS' or 'OTHER'
505+
506+ The resolution is symmetric, when taking THIS, item.THIS is renamed
507+ into item and vice-versa. This takes one of the files as a whole
508+ ignoring every difference that could have been merged cleanly.
509+ """
510+ # To avoid useless copies, we switch item and item.winner_suffix, only
511+ # item will exist after the conflict has been resolved anyway.
512+ item_tid = tt.trans_id_file_id(self.file_id)
513+ item_parent_tid = tt.get_tree_parent(item_tid)
514+ winner_path = self.path + '.' + winner_suffix
515+ winner_tid = tt.trans_id_tree_path(winner_path)
516+ winner_parent_tid = tt.get_tree_parent(winner_tid)
517+ # Switch the paths to preserve the content
518+ tt.adjust_path(osutils.basename(self.path),
519+ winner_parent_tid, winner_tid)
520+ tt.adjust_path(osutils.basename(winner_path),
521+ item_parent_tid, item_tid)
522+ # Associate the file_id to the right content
523+ tt.unversion_file(item_tid)
524+ tt.version_file(winner_tid, file_id=self.file_id)
525+ tt.apply()
526+
527+ def action_auto(self, tree):
528+ # GZ 2012-07-27: Using NotImplementedError to signal that a conflict
529+ # can't be auto resolved does not seem ideal.
530+ try:
531+ kind = tree.kind(self.path)
532+ except errors.NoSuchFile:
533+ return
534+ if kind != 'file':
535+ raise NotImplementedError("Conflict is not a file")
536+ conflict_markers_in_line = self._conflict_re.search
537+ # GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
538+ with tree.get_file(self.path) as f:
539+ for line in f:
540+ if conflict_markers_in_line(line):
541+ raise NotImplementedError("Conflict markers present")
542+
543+ def action_take_this(self, tree):
544+ self._resolve_with_cleanups(tree, 'THIS')
545+
546+ def action_take_other(self, tree):
547+ self._resolve_with_cleanups(tree, 'OTHER')
548+
549+
550+class HandledConflict(Conflict):
551+ """A path problem that has been provisionally resolved.
552+ This is intended to be a base class.
553+ """
554+
555+ rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
556+
557+ def __init__(self, action, path, file_id=None):
558+ Conflict.__init__(self, path, file_id)
559+ self.action = action
560+
561+ def _cmp_list(self):
562+ return Conflict._cmp_list(self) + [self.action]
563+
564+ def as_stanza(self):
565+ s = Conflict.as_stanza(self)
566+ s.add('action', self.action)
567+ return s
568+
569+ def associated_filenames(self):
570+ # Nothing has been generated here
571+ return []
572+
573+
574+class HandledPathConflict(HandledConflict):
575+ """A provisionally-resolved path problem involving two paths.
576+ This is intended to be a base class.
577+ """
578+
579+ rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
580+ " %(file_id)r, %(conflict_file_id)r)"
581+
582+ def __init__(self, action, path, conflict_path, file_id=None,
583+ conflict_file_id=None):
584+ HandledConflict.__init__(self, action, path, file_id)
585+ self.conflict_path = conflict_path
586+ # the factory blindly transfers the Stanza values to __init__,
587+ # so they can be unicode.
588+ if isinstance(conflict_file_id, str):
589+ conflict_file_id = cache_utf8.encode(conflict_file_id)
590+ self.conflict_file_id = conflict_file_id
591+
592+ def _cmp_list(self):
593+ return HandledConflict._cmp_list(self) + [self.conflict_path,
594+ self.conflict_file_id]
595+
596+ def as_stanza(self):
597+ s = HandledConflict.as_stanza(self)
598+ s.add('conflict_path', self.conflict_path)
599+ if self.conflict_file_id is not None:
600+ s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
601+
602+ return s
603+
604+
605+class DuplicateID(HandledPathConflict):
606+ """Two files want the same file_id."""
607+
608+ typestring = 'duplicate id'
609+
610+ format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
611+
612+
613+class DuplicateEntry(HandledPathConflict):
614+ """Two directory entries want to have the same name."""
615+
616+ typestring = 'duplicate'
617+
618+ format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
619+
620+ def action_take_this(self, tree):
621+ tree.remove([self.conflict_path], force=True, keep_files=False)
622+ tree.rename_one(self.path, self.conflict_path)
623+
624+ def action_take_other(self, tree):
625+ tree.remove([self.path], force=True, keep_files=False)
626+
627+
628+class ParentLoop(HandledPathConflict):
629+ """An attempt to create an infinitely-looping directory structure.
630+ This is rare, but can be produced like so:
631+
632+ tree A:
633+ mv foo bar
634+ tree B:
635+ mv bar foo
636+ merge A and B
637+ """
638+
639+ typestring = 'parent loop'
640+
641+ format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
642+
643+ def action_take_this(self, tree):
644+ # just acccept brz proposal
645+ pass
646+
647+ def action_take_other(self, tree):
648+ with tree.transform() as tt:
649+ p_tid = tt.trans_id_file_id(self.file_id)
650+ parent_tid = tt.get_tree_parent(p_tid)
651+ cp_tid = tt.trans_id_file_id(self.conflict_file_id)
652+ cparent_tid = tt.get_tree_parent(cp_tid)
653+ tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
654+ tt.adjust_path(osutils.basename(self.conflict_path),
655+ parent_tid, p_tid)
656+ tt.apply()
657+
658+
659+class UnversionedParent(HandledConflict):
660+ """An attempt to version a file whose parent directory is not versioned.
661+ Typically, the result of a merge where one tree unversioned the directory
662+ and the other added a versioned file to it.
663+ """
664+
665+ typestring = 'unversioned parent'
666+
667+ format = 'Conflict because %(path)s is not versioned, but has versioned'\
668+ ' children. %(action)s.'
669+
670+ # FIXME: We silently do nothing to make tests pass, but most probably the
671+ # conflict shouldn't exist (the long story is that the conflict is
672+ # generated with another one that can be resolved properly) -- vila 091224
673+ def action_take_this(self, tree):
674+ pass
675+
676+ def action_take_other(self, tree):
677+ pass
678+
679+
680+class MissingParent(HandledConflict):
681+ """An attempt to add files to a directory that is not present.
682+ Typically, the result of a merge where THIS deleted the directory and
683+ the OTHER added a file to it.
684+ See also: DeletingParent (same situation, THIS and OTHER reversed)
685+ """
686+
687+ typestring = 'missing parent'
688+
689+ format = 'Conflict adding files to %(path)s. %(action)s.'
690+
691+ def action_take_this(self, tree):
692+ tree.remove([self.path], force=True, keep_files=False)
693+
694+ def action_take_other(self, tree):
695+ # just acccept brz proposal
696+ pass
697+
698+
699+class DeletingParent(HandledConflict):
700+ """An attempt to add files to a directory that is not present.
701+ Typically, the result of a merge where one OTHER deleted the directory and
702+ the THIS added a file to it.
703+ """
704+
705+ typestring = 'deleting parent'
706+
707+ format = "Conflict: can't delete %(path)s because it is not empty. "\
708+ "%(action)s."
709+
710+ # FIXME: It's a bit strange that the default action is not coherent with
711+ # MissingParent from the *user* pov.
712+
713+ def action_take_this(self, tree):
714+ # just acccept brz proposal
715+ pass
716+
717+ def action_take_other(self, tree):
718+ tree.remove([self.path], force=True, keep_files=False)
719+
720+
721+class NonDirectoryParent(HandledConflict):
722+ """An attempt to add files to a directory that is not a directory or
723+ an attempt to change the kind of a directory with files.
724+ """
725+
726+ typestring = 'non-directory parent'
727+
728+ format = "Conflict: %(path)s is not a directory, but has files in it."\
729+ " %(action)s."
730+
731+ # FIXME: .OTHER should be used instead of .new when the conflict is created
732+
733+ def action_take_this(self, tree):
734+ # FIXME: we should preserve that path when the conflict is generated !
735+ if self.path.endswith('.new'):
736+ conflict_path = self.path[:-(len('.new'))]
737+ tree.remove([self.path], force=True, keep_files=False)
738+ tree.add(conflict_path)
739+ else:
740+ raise NotImplementedError(self.action_take_this)
741+
742+ def action_take_other(self, tree):
743+ # FIXME: we should preserve that path when the conflict is generated !
744+ if self.path.endswith('.new'):
745+ conflict_path = self.path[:-(len('.new'))]
746+ tree.remove([conflict_path], force=True, keep_files=False)
747+ tree.rename_one(self.path, conflict_path)
748+ else:
749+ raise NotImplementedError(self.action_take_other)
750+
751+
752+ctype = {}
753+
754+
755+def register_types(*conflict_types):
756+ """Register a Conflict subclass for serialization purposes"""
757+ global ctype
758+ for conflict_type in conflict_types:
759+ ctype[conflict_type.typestring] = conflict_type
760+
761+
762+register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
763+ DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
764+ DeletingParent, NonDirectoryParent)
765
766=== modified file 'breezy/bzr/dirstate.py'
767--- breezy/bzr/dirstate.py 2020-07-18 23:14:00 +0000
768+++ breezy/bzr/dirstate.py 2020-08-23 01:05:31 +0000
769@@ -243,7 +243,7 @@
770 trace,
771 urlutils,
772 )
773-from ..tree import TreeChange
774+from .inventorytree import InventoryTreeChange
775
776
777 # This is the Windows equivalent of ENOTDIR
778@@ -3759,7 +3759,7 @@
779 else:
780 path_u = self.utf8_decode(path)[0]
781 source_kind = DirState._minikind_to_kind[source_minikind]
782- return TreeChange(
783+ return InventoryTreeChange(
784 entry[0][2],
785 (old_path_u, path_u),
786 content_change,
787@@ -3788,7 +3788,7 @@
788 and stat.S_IEXEC & path_info[3].st_mode)
789 else:
790 target_exec = target_details[3]
791- return TreeChange(
792+ return InventoryTreeChange(
793 entry[0][2],
794 (None, self.utf8_decode(path)[0]),
795 True,
796@@ -3799,7 +3799,7 @@
797 (None, target_exec)), True
798 else:
799 # Its a missing file, report it as such.
800- return TreeChange(
801+ return InventoryTreeChange(
802 entry[0][2],
803 (None, self.utf8_decode(path)[0]),
804 False,
805@@ -3819,7 +3819,7 @@
806 self.source_index, path_utf8=entry[0][0])[0][2]
807 if parent_id == entry[0][2]:
808 parent_id = None
809- return TreeChange(
810+ return InventoryTreeChange(
811 entry[0][2],
812 (self.utf8_decode(old_path)[0], None),
813 True,
814@@ -3961,7 +3961,7 @@
815 new_executable = bool(
816 stat.S_ISREG(root_dir_info[3].st_mode)
817 and stat.S_IEXEC & root_dir_info[3].st_mode)
818- yield TreeChange(
819+ yield InventoryTreeChange(
820 None,
821 (None, current_root_unicode),
822 True,
823@@ -4044,7 +4044,7 @@
824 new_executable = bool(
825 stat.S_ISREG(current_path_info[3].st_mode)
826 and stat.S_IEXEC & current_path_info[3].st_mode)
827- yield TreeChange(
828+ yield InventoryTreeChange(
829 None,
830 (None, utf8_decode(current_path_info[0])[0]),
831 True,
832@@ -4180,7 +4180,7 @@
833 except UnicodeDecodeError:
834 raise errors.BadFilenameEncoding(
835 current_path_info[0], osutils._fs_enc)
836- yield TreeChange(
837+ yield InventoryTreeChange(
838 None,
839 (None, relpath_unicode),
840 True,
841
842=== modified file 'breezy/bzr/inventorytree.py'
843--- breezy/bzr/inventorytree.py 2020-07-18 23:14:00 +0000
844+++ breezy/bzr/inventorytree.py 2020-08-23 01:05:31 +0000
845@@ -67,6 +67,60 @@
846 )
847
848
849+class InventoryTreeChange(TreeChange):
850+
851+ __slots__ = TreeChange.__slots__ + ['file_id', 'parent_id']
852+
853+ def __init__(self, file_id, path, changed_content, versioned, parent_id,
854+ name, kind, executable, copied=False):
855+ self.file_id = file_id
856+ self.parent_id = parent_id
857+ super(InventoryTreeChange, self).__init__(
858+ path=path, changed_content=changed_content, versioned=versioned,
859+ name=name, kind=kind, executable=executable, copied=copied)
860+
861+ def __repr__(self):
862+ return "%s%r" % (self.__class__.__name__, self._as_tuple())
863+
864+ def _as_tuple(self):
865+ return (self.file_id, self.path, self.changed_content, self.versioned,
866+ self.parent_id, self.name, self.kind, self.executable, self.copied)
867+
868+ def __eq__(self, other):
869+ if isinstance(other, TreeChange):
870+ return self._as_tuple() == other._as_tuple()
871+ if isinstance(other, tuple):
872+ return self._as_tuple() == other
873+ return False
874+
875+ def __lt__(self, other):
876+ return self._as_tuple() < other._as_tuple()
877+
878+ def meta_modified(self):
879+ if self.versioned == (True, True):
880+ return (self.executable[0] != self.executable[1])
881+ return False
882+
883+ def is_reparented(self):
884+ return self.parent_id[0] != self.parent_id[1]
885+
886+ @property
887+ def renamed(self):
888+ return (
889+ not self.copied and
890+ None not in self.name and
891+ None not in self.parent_id and
892+ (self.name[0] != self.name[1] or self.parent_id[0] != self.parent_id[1]))
893+
894+ def discard_new(self):
895+ return self.__class__(
896+ self.file_id, (self.path[0], None), self.changed_content,
897+ (self.versioned[0], None), (self.parent_id[0], None),
898+ (self.name[0], None), (self.kind[0], None),
899+ (self.executable[0], None),
900+ copied=False)
901+
902+
903 class InventoryTree(Tree):
904 """A tree that relies on an inventory for its metadata.
905
906@@ -418,6 +472,51 @@
907 inv.apply_delta(changes)
908 self._write_inventory(inv)
909
910+ def has_changes(self, _from_tree=None):
911+ """Quickly check that the tree contains at least one commitable change.
912+
913+ :param _from_tree: tree to compare against to find changes (default to
914+ the basis tree and is intended to be used by tests).
915+
916+ :return: True if a change is found. False otherwise
917+ """
918+ with self.lock_read():
919+ # Check pending merges
920+ if len(self.get_parent_ids()) > 1:
921+ return True
922+ if _from_tree is None:
923+ _from_tree = self.basis_tree()
924+ changes = self.iter_changes(_from_tree)
925+ if self.supports_symlinks():
926+ # Fast path for has_changes.
927+ try:
928+ change = next(changes)
929+ # Exclude root (talk about black magic... --vila 20090629)
930+ if change.parent_id == (None, None):
931+ change = next(changes)
932+ return True
933+ except StopIteration:
934+ # No changes
935+ return False
936+ else:
937+ # Slow path for has_changes.
938+ # Handle platforms that do not support symlinks in the
939+ # conditional below. This is slower than the try/except
940+ # approach below that but we don't have a choice as we
941+ # need to be sure that all symlinks are removed from the
942+ # entire changeset. This is because in platforms that
943+ # do not support symlinks, they show up as None in the
944+ # working copy as compared to the repository.
945+ # Also, exclude root as mention in the above fast path.
946+ changes = filter(
947+ lambda c: c[6][0] != 'symlink' and c[4] != (None, None),
948+ changes)
949+ try:
950+ next(iter(changes))
951+ except StopIteration:
952+ return False
953+ return True
954+
955 def _fix_case_of_inventory_path(self, path):
956 """If our tree isn't case sensitive, return the canonical path"""
957 if not self.case_sensitive:
958@@ -906,27 +1005,25 @@
959 if top_id is None:
960 pending = []
961 else:
962- pending = [(prefix, '', _directory, None, top_id, None)]
963+ pending = [(prefix, top_id)]
964 while pending:
965 dirblock = []
966- currentdir = pending.pop()
967- # 0 - relpath, 1- basename, 2- kind, 3- stat, id, v-kind
968- if currentdir[0]:
969- relroot = currentdir[0] + '/'
970+ root, file_id = pending.pop()
971+ if root:
972+ relroot = root + '/'
973 else:
974 relroot = ""
975 # FIXME: stash the node in pending
976- entry = inv.get_entry(currentdir[4])
977+ entry = inv.get_entry(file_id)
978+ subdirs = []
979 for name, child in entry.sorted_children():
980 toppath = relroot + name
981- dirblock.append((toppath, name, child.kind, None,
982- child.file_id, child.kind
983- ))
984- yield (currentdir[0], entry.file_id), dirblock
985+ dirblock.append((toppath, name, child.kind, None, child.kind))
986+ if child.kind == _directory:
987+ subdirs.append((toppath, child.file_id))
988+ yield root, dirblock
989 # push the user specified dirs from dirblock
990- for dir in reversed(dirblock):
991- if dir[2] == _directory:
992- pending.append(dir)
993+ pending.extend(reversed(subdirs))
994
995 def iter_files_bytes(self, desired_files):
996 """See Tree.iter_files_bytes.
997@@ -1047,7 +1144,7 @@
998 changes = True
999 else:
1000 changes = False
1001- return TreeChange(
1002+ return InventoryTreeChange(
1003 file_id, (source_path, target_path), changed_content,
1004 versioned, parent, name, kind, executable), changes
1005
1006@@ -1141,7 +1238,7 @@
1007 target_kind, target_executable, target_stat = \
1008 self.target._comparison_data(
1009 fake_entry, unversioned_path[1])
1010- yield TreeChange(
1011+ yield InventoryTreeChange(
1012 None, (None, unversioned_path[1]), True, (False, False),
1013 (None, None),
1014 (None, unversioned_path[0][-1]),
1015@@ -1178,7 +1275,7 @@
1016 unversioned_path = all_unversioned.popleft()
1017 to_kind, to_executable, to_stat = \
1018 self.target._comparison_data(fake_entry, unversioned_path[1])
1019- yield TreeChange(
1020+ yield InventoryTreeChange(
1021 None, (None, unversioned_path[1]), True, (False, False),
1022 (None, None),
1023 (None, unversioned_path[0][-1]),
1024@@ -1204,7 +1301,7 @@
1025 changed_content = from_kind is not None
1026 # the parent's path is necessarily known at this point.
1027 changed_file_ids.append(file_id)
1028- yield TreeChange(
1029+ yield InventoryTreeChange(
1030 file_id, (path, to_path), changed_content, versioned, parent,
1031 name, kind, executable)
1032 changed_file_ids = set(changed_file_ids)
1033@@ -1390,7 +1487,7 @@
1034 # FIXME: nested tree support
1035 for result in self.target.root_inventory.iter_changes(
1036 self.source.root_inventory):
1037- result = TreeChange(*result)
1038+ result = InventoryTreeChange(*result)
1039 if specific_file_ids is not None:
1040 if result.file_id not in specific_file_ids:
1041 # A change from the whole tree that we don't want to show yet.
1042@@ -1417,7 +1514,7 @@
1043 entry.file_id not in specific_file_ids):
1044 continue
1045 if entry.file_id not in changed_file_ids:
1046- yield TreeChange(
1047+ yield InventoryTreeChange(
1048 entry.file_id,
1049 (relpath, relpath), # Not renamed
1050 False, # Not modified
1051
1052=== modified file 'breezy/bzr/tests/__init__.py'
1053--- breezy/bzr/tests/__init__.py 2020-06-30 00:19:29 +0000
1054+++ breezy/bzr/tests/__init__.py 2020-08-23 01:05:31 +0000
1055@@ -63,6 +63,7 @@
1056 'test_bzrdir',
1057 'test_chk_map',
1058 'test_chk_serializer',
1059+ 'test_conflicts',
1060 'test_generate_ids',
1061 'test_groupcompress',
1062 'test_index',
1063
1064=== added file 'breezy/bzr/tests/test_conflicts.py'
1065--- breezy/bzr/tests/test_conflicts.py 1970-01-01 00:00:00 +0000
1066+++ breezy/bzr/tests/test_conflicts.py 2020-08-23 01:05:31 +0000
1067@@ -0,0 +1,133 @@
1068+# Copyright (C) 2005-2011 Canonical Ltd
1069+# Copyright (C) 2018-2020 Breezy Developers
1070+#
1071+# This program is free software; you can redistribute it and/or modify
1072+# it under the terms of the GNU General Public License as published by
1073+# the Free Software Foundation; either version 2 of the License, or
1074+# (at your option) any later version.
1075+#
1076+# This program is distributed in the hope that it will be useful,
1077+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1078+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1079+# GNU General Public License for more details.
1080+#
1081+# You should have received a copy of the GNU General Public License
1082+# along with this program; if not, write to the Free Software
1083+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1084+
1085+import os
1086+
1087+from ...conflicts import resolve
1088+from ... import (
1089+ tests,
1090+ )
1091+
1092+from ...tests import (
1093+ scenarios,
1094+ )
1095+from ...tests.test_conflicts import vary_by_conflicts
1096+
1097+
1098+from .. import conflicts as bzr_conflicts
1099+
1100+
1101+load_tests = scenarios.load_tests_apply_scenarios
1102+
1103+
1104+class TestPerConflict(tests.TestCase):
1105+
1106+ scenarios = scenarios.multiply_scenarios(vary_by_conflicts())
1107+
1108+ def test_stringification(self):
1109+ text = str(self.conflict)
1110+ self.assertContainsString(text, self.conflict.path)
1111+ self.assertContainsString(text.lower(), "conflict")
1112+ self.assertContainsString(repr(self.conflict),
1113+ self.conflict.__class__.__name__)
1114+
1115+ def test_stanza_roundtrip(self):
1116+ p = self.conflict
1117+ o = bzr_conflicts.Conflict.factory(**p.as_stanza().as_dict())
1118+ self.assertEqual(o, p)
1119+
1120+ self.assertIsInstance(o.path, str)
1121+
1122+ if o.file_id is not None:
1123+ self.assertIsInstance(o.file_id, bytes)
1124+
1125+ conflict_path = getattr(o, 'conflict_path', None)
1126+ if conflict_path is not None:
1127+ self.assertIsInstance(conflict_path, str)
1128+
1129+ conflict_file_id = getattr(o, 'conflict_file_id', None)
1130+ if conflict_file_id is not None:
1131+ self.assertIsInstance(conflict_file_id, bytes)
1132+
1133+ def test_stanzification(self):
1134+ stanza = self.conflict.as_stanza()
1135+ if 'file_id' in stanza:
1136+ # In Stanza form, the file_id has to be unicode.
1137+ self.assertStartsWith(stanza['file_id'], u'\xeed')
1138+ self.assertStartsWith(stanza['path'], u'p\xe5th')
1139+ if 'conflict_path' in stanza:
1140+ self.assertStartsWith(stanza['conflict_path'], u'p\xe5th')
1141+ if 'conflict_file_id' in stanza:
1142+ self.assertStartsWith(stanza['conflict_file_id'], u'\xeed')
1143+
1144+
1145+class TestConflicts(tests.TestCaseWithTransport):
1146+
1147+ def test_resolve_conflict_dir(self):
1148+ tree = self.make_branch_and_tree('.')
1149+ self.build_tree_contents([('hello', b'hello world4'),
1150+ ('hello.THIS', b'hello world2'),
1151+ ('hello.BASE', b'hello world1'),
1152+ ])
1153+ os.mkdir('hello.OTHER')
1154+ tree.add('hello', b'q')
1155+ l = bzr_conflicts.ConflictList([bzr_conflicts.TextConflict('hello')])
1156+ l.remove_files(tree)
1157+
1158+ def test_select_conflicts(self):
1159+ tree = self.make_branch_and_tree('.')
1160+ clist = bzr_conflicts.ConflictList
1161+
1162+ def check_select(not_selected, selected, paths, **kwargs):
1163+ self.assertEqual(
1164+ (not_selected, selected),
1165+ tree_conflicts.select_conflicts(tree, paths, **kwargs))
1166+
1167+ foo = bzr_conflicts.ContentsConflict('foo')
1168+ bar = bzr_conflicts.ContentsConflict('bar')
1169+ tree_conflicts = clist([foo, bar])
1170+
1171+ check_select(clist([bar]), clist([foo]), ['foo'])
1172+ check_select(clist(), tree_conflicts,
1173+ [''], ignore_misses=True, recurse=True)
1174+
1175+ foobaz = bzr_conflicts.ContentsConflict('foo/baz')
1176+ tree_conflicts = clist([foobaz, bar])
1177+
1178+ check_select(clist([bar]), clist([foobaz]),
1179+ ['foo'], ignore_misses=True, recurse=True)
1180+
1181+ qux = bzr_conflicts.PathConflict('qux', 'foo/baz')
1182+ tree_conflicts = clist([qux])
1183+
1184+ check_select(clist(), tree_conflicts,
1185+ ['foo'], ignore_misses=True, recurse=True)
1186+ check_select(tree_conflicts, clist(), ['foo'], ignore_misses=True)
1187+
1188+ def test_resolve_conflicts_recursive(self):
1189+ tree = self.make_branch_and_tree('.')
1190+ self.build_tree(['dir/', 'dir/hello'])
1191+ tree.add(['dir', 'dir/hello'])
1192+
1193+ dirhello = [bzr_conflicts.TextConflict('dir/hello')]
1194+ tree.set_conflicts(dirhello)
1195+
1196+ resolve(tree, ['dir'], recursive=False, ignore_misses=True)
1197+ self.assertEqual(dirhello, tree.conflicts())
1198+
1199+ resolve(tree, ['dir'], recursive=True, ignore_misses=True)
1200+ self.assertEqual(bzr_conflicts.ConflictList([]), tree.conflicts())
1201
1202=== modified file 'breezy/bzr/transform.py'
1203--- breezy/bzr/transform.py 2020-07-20 02:02:23 +0000
1204+++ breezy/bzr/transform.py 2020-08-23 01:05:31 +0000
1205@@ -24,6 +24,7 @@
1206
1207 from .. import (
1208 annotate,
1209+ conflicts,
1210 errors,
1211 lock,
1212 multiparent,
1213@@ -52,7 +53,8 @@
1214 MalformedTransform,
1215 PreviewTree,
1216 )
1217-from ..tree import TreeChange
1218+from .conflicts import Conflict
1219+
1220 from . import (
1221 inventory,
1222 inventorytree,
1223@@ -108,7 +110,7 @@
1224
1225 def create_path(self, name, parent):
1226 """Assign a transaction id to a new path"""
1227- trans_id = self._assign_id()
1228+ trans_id = self.assign_id()
1229 unique_add(self._new_name, trans_id, name)
1230 unique_add(self._new_parent, trans_id, parent)
1231 return trans_id
1232@@ -223,7 +225,7 @@
1233 if file_id in self._non_present_ids:
1234 return self._non_present_ids[file_id]
1235 else:
1236- trans_id = self._assign_id()
1237+ trans_id = self.assign_id()
1238 self._non_present_ids[file_id] = trans_id
1239 return trans_id
1240 else:
1241@@ -296,7 +298,7 @@
1242 if value == trans_id:
1243 return key
1244
1245- def find_conflicts(self):
1246+ def find_raw_conflicts(self):
1247 """Find any violations of inventory or filesystem invariants"""
1248 if self._done is True:
1249 raise ReusingTransform()
1250@@ -315,7 +317,7 @@
1251 return conflicts
1252
1253 def _check_malformed(self):
1254- conflicts = self.find_conflicts()
1255+ conflicts = self.find_raw_conflicts()
1256 if len(conflicts) != 0:
1257 raise MalformedTransform(conflicts=conflicts)
1258
1259@@ -703,7 +705,7 @@
1260 """Produce output in the same format as Tree.iter_changes.
1261
1262 Will produce nonsensical results if invoked while inventory/filesystem
1263- conflicts (as reported by TreeTransform.find_conflicts()) are present.
1264+ conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
1265
1266 This reads the Transform, but only reproduces changes involving a
1267 file_id. Files that are not versioned in either of the FROM or TO
1268@@ -755,7 +757,7 @@
1269 and from_executable == to_executable):
1270 continue
1271 results.append(
1272- TreeChange(
1273+ inventorytree.InventoryTreeChange(
1274 file_id, (from_path, to_path), modified,
1275 (from_versioned, to_versioned),
1276 (from_parent, to_parent),
1277@@ -1010,6 +1012,100 @@
1278 """
1279 raise NotImplementedError(self.apply)
1280
1281+ def cook_conflicts(self, raw_conflicts):
1282+ """Generate a list of cooked conflicts, sorted by file path"""
1283+ content_conflict_file_ids = set()
1284+ cooked_conflicts = list(iter_cook_conflicts(raw_conflicts, self))
1285+ for c in cooked_conflicts:
1286+ if c.typestring == 'contents conflict':
1287+ content_conflict_file_ids.add(c.file_id)
1288+ # We want to get rid of path conflicts when a corresponding contents
1289+ # conflict exists. This can occur when one branch deletes a file while
1290+ # the other renames *and* modifies it. In this case, the content
1291+ # conflict is enough.
1292+ cooked_conflicts = [
1293+ c for c in cooked_conflicts
1294+ if c.typestring != 'path conflict' or
1295+ c.file_id not in content_conflict_file_ids]
1296+ return sorted(cooked_conflicts, key=Conflict.sort_key)
1297+
1298+
1299+def cook_path_conflict(
1300+ tt, fp, conflict_type, trans_id, file_id, this_parent, this_name,
1301+ other_parent, other_name):
1302+ if this_parent is None or this_name is None:
1303+ this_path = '<deleted>'
1304+ else:
1305+ parent_path = fp.get_path(tt.trans_id_file_id(this_parent))
1306+ this_path = osutils.pathjoin(parent_path, this_name)
1307+ if other_parent is None or other_name is None:
1308+ other_path = '<deleted>'
1309+ else:
1310+ try:
1311+ parent_path = fp.get_path(tt.trans_id_file_id(other_parent))
1312+ except NoFinalPath:
1313+ # The other entry was in a path that doesn't exist in our tree.
1314+ # Put it in the root.
1315+ parent_path = ''
1316+ other_path = osutils.pathjoin(parent_path, other_name)
1317+ return Conflict.factory(
1318+ conflict_type, path=this_path,
1319+ conflict_path=other_path,
1320+ file_id=file_id)
1321+
1322+
1323+def cook_content_conflict(tt, fp, conflict_type, trans_ids):
1324+ for trans_id in trans_ids:
1325+ file_id = tt.final_file_id(trans_id)
1326+ if file_id is not None:
1327+ # Ok we found the relevant file-id
1328+ break
1329+ path = fp.get_path(trans_id)
1330+ for suffix in ('.BASE', '.THIS', '.OTHER'):
1331+ if path.endswith(suffix):
1332+ # Here is the raw path
1333+ path = path[:-len(suffix)]
1334+ break
1335+ return Conflict.factory(conflict_type, path=path, file_id=file_id)
1336+
1337+
1338+def cook_text_conflict(tt, fp, conflict_type, trans_id):
1339+ path = fp.get_path(trans_id)
1340+ file_id = tt.final_file_id(trans_id)
1341+ return Conflict.factory(conflict_type, path=path, file_id=file_id)
1342+
1343+
1344+CONFLICT_COOKERS = {
1345+ 'path conflict': cook_path_conflict,
1346+ 'text conflict': cook_text_conflict,
1347+ 'contents conflict': cook_content_conflict,
1348+}
1349+
1350+def iter_cook_conflicts(raw_conflicts, tt):
1351+ fp = FinalPaths(tt)
1352+ for conflict in raw_conflicts:
1353+ c_type = conflict[0]
1354+ try:
1355+ cooker = CONFLICT_COOKERS[c_type]
1356+ except KeyError:
1357+ action = conflict[1]
1358+ modified_path = fp.get_path(conflict[2])
1359+ modified_id = tt.final_file_id(conflict[2])
1360+ if len(conflict) == 3:
1361+ yield Conflict.factory(
1362+ c_type, action=action, path=modified_path, file_id=modified_id)
1363+
1364+ else:
1365+ conflicting_path = fp.get_path(conflict[3])
1366+ conflicting_id = tt.final_file_id(conflict[3])
1367+ yield Conflict.factory(
1368+ c_type, action=action, path=modified_path,
1369+ file_id=modified_id,
1370+ conflict_path=conflicting_path,
1371+ conflict_file_id=conflicting_id)
1372+ else:
1373+ yield cooker(tt, fp, *conflict)
1374+
1375
1376 class DiskTreeTransform(TreeTransformBase):
1377 """Tree transform storing its contents on disk."""
1378@@ -1497,8 +1593,8 @@
1379 conflicts.append(('duplicate id', old_trans_id, trans_id))
1380 return conflicts
1381
1382- def find_conflicts(self):
1383- conflicts = super(InventoryTreeTransform, self).find_conflicts()
1384+ def find_raw_conflicts(self):
1385+ conflicts = super(InventoryTreeTransform, self).find_raw_conflicts()
1386 conflicts.extend(self._duplicate_ids())
1387 return conflicts
1388
1389@@ -1816,6 +1912,9 @@
1390 self._iter_changes_cache = {
1391 c.file_id: c for c in self._transform.iter_changes()}
1392
1393+ def supports_setting_file_ids(self):
1394+ return True
1395+
1396 def supports_tree_reference(self):
1397 # TODO(jelmer): Support tree references in PreviewTree.
1398 # return self._transform._tree.supports_tree_reference()
1399@@ -2028,6 +2127,7 @@
1400 limbo_name = tt._limbo_name(trans_id)
1401 if trans_id in tt._new_reference_revision:
1402 kind = 'tree-reference'
1403+ link_or_sha1 = tt._new_reference_revision
1404 if kind == 'file':
1405 statval = os.lstat(limbo_name)
1406 size = statval.st_size
1407@@ -2071,21 +2171,28 @@
1408 file_id = self.path2id(path)
1409 changes = self._iter_changes_cache.get(file_id)
1410 if changes is None:
1411- get_old = True
1412+ if file_id is None:
1413+ old_path = None
1414+ else:
1415+ old_path = self._transform._tree.id2path(file_id)
1416 else:
1417- changed_content, versioned, kind = (
1418- changes.changed_content, changes.versioned, changes.kind)
1419- if kind[1] is None:
1420+ if changes.kind[1] is None:
1421 return None
1422- get_old = (kind[0] == 'file' and versioned[0])
1423- if get_old:
1424+ if changes.kind[0] == 'file' and changes.versioned[0]:
1425+ old_path = changes.path[0]
1426+ else:
1427+ old_path = None
1428+ if old_path is not None:
1429 old_annotation = self._transform._tree.annotate_iter(
1430- path, default_revision=default_revision)
1431+ old_path, default_revision=default_revision)
1432 else:
1433 old_annotation = []
1434 if changes is None:
1435- return old_annotation
1436- if not changed_content:
1437+ if old_path is None:
1438+ return None
1439+ else:
1440+ return old_annotation
1441+ if not changes.changed_content:
1442 return old_annotation
1443 # TODO: This is doing something similar to what WT.annotate_iter is
1444 # doing, however it fails slightly because it doesn't know what
1445@@ -2095,7 +2202,7 @@
1446 # It would be nice to be able to use the new Annotator based
1447 # approach, as well.
1448 return annotate.reannotate([old_annotation],
1449- self.get_file(path).readlines(),
1450+ self.get_file_lines(path),
1451 default_revision)
1452
1453 def walkdirs(self, prefix=''):
1454@@ -2117,14 +2224,14 @@
1455 else:
1456 kind = 'unknown'
1457 versioned_kind = self._transform._tree.stored_kind(
1458- self._transform._tree.id2path(file_id))
1459+ path_from_root)
1460 if versioned_kind == 'directory':
1461 subdirs.append(child_id)
1462 children.append((path_from_root, basename, kind, None,
1463- file_id, versioned_kind))
1464+ versioned_kind))
1465 children.sort()
1466 if parent_path.startswith(prefix):
1467- yield (parent_path, parent_file_id), children
1468+ yield parent_path, children
1469 pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1470 reverse=True))
1471
1472
1473=== modified file 'breezy/bzr/vf_repository.py'
1474--- breezy/bzr/vf_repository.py 2020-07-18 23:14:00 +0000
1475+++ breezy/bzr/vf_repository.py 2020-08-23 01:05:31 +0000
1476@@ -80,7 +80,7 @@
1477 from ..trace import (
1478 mutter
1479 )
1480-from ..tree import TreeChange
1481+from .inventorytree import InventoryTreeChange
1482
1483
1484 class VersionedFileRepositoryFormat(RepositoryFormat):
1485@@ -399,7 +399,7 @@
1486 # by the user. So we discard this change.
1487 pass
1488 else:
1489- change = TreeChange(
1490+ change = InventoryTreeChange(
1491 file_id,
1492 (basis_inv.id2path(file_id), tree.id2path(file_id)),
1493 False, (True, True),
1494
1495=== modified file 'breezy/bzr/workingtree.py'
1496--- breezy/bzr/workingtree.py 2020-07-18 23:14:00 +0000
1497+++ breezy/bzr/workingtree.py 2020-08-23 01:05:31 +0000
1498@@ -60,6 +60,7 @@
1499 rio as _mod_rio,
1500 )
1501 from breezy.bzr import (
1502+ conflicts as _mod_bzr_conflicts,
1503 inventory,
1504 serializer,
1505 xml5,
1506@@ -403,8 +404,8 @@
1507 def recurse_directory_to_add_files(directory):
1508 # Recurse directory and add all files
1509 # so we can check if they have changed.
1510- for parent_info, file_infos in self.walkdirs(directory):
1511- for relpath, basename, kind, lstat, fileid, kind in file_infos:
1512+ for parent_path, file_infos in self.walkdirs(directory):
1513+ for relpath, basename, kind, lstat, kind in file_infos:
1514 # Is it versioned or ignored?
1515 if self.is_versioned(relpath):
1516 # Add nested content for deletion.
1517@@ -573,23 +574,24 @@
1518 return xml7.serializer_v7.write_inventory_to_lines(inventory)
1519
1520 def set_conflicts(self, conflicts):
1521+ conflict_list = _mod_bzr_conflicts.ConflictList(conflicts)
1522 with self.lock_tree_write():
1523- self._put_rio('conflicts', conflicts.to_stanzas(),
1524+ self._put_rio('conflicts', conflict_list.to_stanzas(),
1525 CONFLICT_HEADER_1)
1526
1527 def add_conflicts(self, new_conflicts):
1528 with self.lock_tree_write():
1529 conflict_set = set(self.conflicts())
1530 conflict_set.update(set(list(new_conflicts)))
1531- self.set_conflicts(_mod_conflicts.ConflictList(
1532- sorted(conflict_set, key=_mod_conflicts.Conflict.sort_key)))
1533+ self.set_conflicts(
1534+ sorted(conflict_set, key=_mod_bzr_conflicts.Conflict.sort_key))
1535
1536 def conflicts(self):
1537 with self.lock_read():
1538 try:
1539 confile = self._transport.get('conflicts')
1540 except errors.NoSuchFile:
1541- return _mod_conflicts.ConflictList()
1542+ return _mod_bzr_conflicts.ConflictList()
1543 try:
1544 try:
1545 if next(confile) != CONFLICT_HEADER_1 + b'\n':
1546@@ -597,7 +599,7 @@
1547 except StopIteration:
1548 raise errors.ConflictFormatError()
1549 reader = _mod_rio.RioReader(confile)
1550- return _mod_conflicts.ConflictList.from_stanzas(reader)
1551+ return _mod_bzr_conflicts.ConflictList.from_stanzas(reader)
1552 finally:
1553 confile.close()
1554
1555@@ -1562,8 +1564,8 @@
1556 """Walk the directories of this tree.
1557
1558 returns a generator which yields items in the form:
1559- ((curren_directory_path, fileid),
1560- [(file1_path, file1_name, file1_kind, (lstat), file1_id,
1561+ (current_directory_path,
1562+ [(file1_path, file1_name, file1_kind, (lstat),
1563 file1_kind), ... ])
1564
1565 This API returns a generator, which is only valid during the current
1566@@ -1626,20 +1628,20 @@
1567
1568 if direction > 0:
1569 # disk is before inventory - unknown
1570- dirblock = [(relpath, basename, kind, stat, None, None) for
1571+ dirblock = [(relpath, basename, kind, stat, None) for
1572 relpath, basename, kind, stat, top_path in
1573 cur_disk_dir_content]
1574- yield (cur_disk_dir_relpath, None), dirblock
1575+ yield cur_disk_dir_relpath, dirblock
1576 try:
1577 current_disk = next(disk_iterator)
1578 except StopIteration:
1579 disk_finished = True
1580 elif direction < 0:
1581 # inventory is before disk - missing.
1582- dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
1583+ dirblock = [(relpath, basename, 'unknown', None, kind)
1584 for relpath, basename, dkind, stat, fileid, kind in
1585 current_inv[1]]
1586- yield (current_inv[0][0], current_inv[0][1]), dirblock
1587+ yield current_inv[0][0], dirblock
1588 try:
1589 current_inv = next(inventory_iterator)
1590 except StopIteration:
1591@@ -1657,23 +1659,20 @@
1592 # versioned, present file
1593 dirblock.append((inv_row[0],
1594 inv_row[1], disk_row[2],
1595- disk_row[3], inv_row[4],
1596- inv_row[5]))
1597+ disk_row[3], inv_row[5]))
1598 elif len(path_elements[0]) == 5:
1599 # unknown disk file
1600 dirblock.append(
1601 (path_elements[0][0], path_elements[0][1],
1602- path_elements[0][2], path_elements[0][3], None,
1603- None))
1604+ path_elements[0][2], path_elements[0][3], None))
1605 elif len(path_elements[0]) == 6:
1606 # versioned, absent file.
1607 dirblock.append(
1608 (path_elements[0][0], path_elements[0][1],
1609- 'unknown', None, path_elements[0][4],
1610- path_elements[0][5]))
1611+ 'unknown', None, path_elements[0][5]))
1612 else:
1613 raise NotImplementedError('unreachable code')
1614- yield current_inv[0], dirblock
1615+ yield current_inv[0][0], dirblock
1616 try:
1617 current_inv = next(inventory_iterator)
1618 except StopIteration:
1619@@ -1818,6 +1817,52 @@
1620 self.path2id(path),
1621 path, possible_transports=possible_transports)
1622
1623+ def has_changes(self, _from_tree=None):
1624+ """Quickly check that the tree contains at least one commitable change.
1625+
1626+ :param _from_tree: tree to compare against to find changes (default to
1627+ the basis tree and is intended to be used by tests).
1628+
1629+ :return: True if a change is found. False otherwise
1630+ """
1631+ with self.lock_read():
1632+ # Check pending merges
1633+ if len(self.get_parent_ids()) > 1:
1634+ return True
1635+ if _from_tree is None:
1636+ _from_tree = self.basis_tree()
1637+ changes = self.iter_changes(_from_tree)
1638+ if self.supports_symlinks():
1639+ # Fast path for has_changes.
1640+ try:
1641+ change = next(changes)
1642+ # Exclude root (talk about black magic... --vila 20090629)
1643+ if change.parent_id == (None, None):
1644+ change = next(changes)
1645+ return True
1646+ except StopIteration:
1647+ # No changes
1648+ return False
1649+ else:
1650+ # Slow path for has_changes.
1651+ # Handle platforms that do not support symlinks in the
1652+ # conditional below. This is slower than the try/except
1653+ # approach below that but we don't have a choice as we
1654+ # need to be sure that all symlinks are removed from the
1655+ # entire changeset. This is because in platforms that
1656+ # do not support symlinks, they show up as None in the
1657+ # working copy as compared to the repository.
1658+ # Also, exclude root as mention in the above fast path.
1659+ changes = filter(
1660+ lambda c: c[6][0] != 'symlink' and c[4] != (None, None),
1661+ changes)
1662+ try:
1663+ next(iter(changes))
1664+ except StopIteration:
1665+ return False
1666+ return True
1667+
1668+
1669
1670 class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
1671 """Base class for working trees that live in bzr meta directories."""
1672
1673=== modified file 'breezy/bzr/workingtree_4.py'
1674--- breezy/bzr/workingtree_4.py 2020-07-18 23:14:00 +0000
1675+++ breezy/bzr/workingtree_4.py 2020-08-23 01:05:31 +0000
1676@@ -465,7 +465,13 @@
1677
1678 def get_reference_revision(self, path):
1679 # referenced tree's revision is whatever's currently there
1680- return self.get_nested_tree(path).last_revision()
1681+ try:
1682+ return self.get_nested_tree(path).last_revision()
1683+ except errors.NotBranchError:
1684+ entry = self._get_entry(path=path)
1685+ if entry == (None, None):
1686+ return False
1687+ return entry[1][0][1]
1688
1689 def get_nested_tree(self, path):
1690 return WorkingTree.open(self.abspath(path))
1691@@ -2157,16 +2163,15 @@
1692 relroot = ""
1693 # FIXME: stash the node in pending
1694 entry = inv.get_entry(file_id)
1695+ subdirs = []
1696 for name, child in entry.sorted_children():
1697 toppath = relroot + name
1698- dirblock.append((toppath, name, child.kind, None,
1699- child.file_id, child.kind
1700- ))
1701- yield (relpath, entry.file_id), dirblock
1702+ dirblock.append((toppath, name, child.kind, None, child.kind))
1703+ if child.kind == _directory:
1704+ subdirs.append((toppath, child.file_id))
1705+ yield relpath, dirblock
1706 # push the user specified dirs from dirblock
1707- for dir in reversed(dirblock):
1708- if dir[2] == _directory:
1709- pending.append((dir[0], dir[4]))
1710+ pending.extend(reversed(subdirs))
1711
1712
1713 class InterDirStateTree(InterInventoryTree):
1714
1715=== modified file 'breezy/commit.py'
1716--- breezy/commit.py 2020-06-19 21:26:53 +0000
1717+++ breezy/commit.py 2020-08-23 01:05:31 +0000
1718@@ -67,7 +67,6 @@
1719 minimum_path_selection,
1720 )
1721 from .trace import mutter, note, is_quiet
1722-from .tree import TreeChange
1723 from .urlutils import unescape_for_display
1724 from .i18n import gettext
1725
1726
1727=== modified file 'breezy/conflicts.py'
1728--- breezy/conflicts.py 2020-07-18 23:14:00 +0000
1729+++ breezy/conflicts.py 2020-08-23 01:05:31 +0000
1730@@ -17,18 +17,14 @@
1731 # TODO: 'brz resolve' should accept a directory name and work from that
1732 # point down
1733
1734+import errno
1735 import os
1736 import re
1737
1738 from .lazy_import import lazy_import
1739 lazy_import(globals(), """
1740-import errno
1741
1742 from breezy import (
1743- osutils,
1744- rio,
1745- trace,
1746- transform,
1747 workingtree,
1748 )
1749 from breezy.i18n import gettext, ngettext
1750@@ -38,13 +34,12 @@
1751 errors,
1752 commands,
1753 option,
1754+ osutils,
1755 registry,
1756+ trace,
1757 )
1758
1759
1760-CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
1761-
1762-
1763 class cmd_conflicts(commands.Command):
1764 __doc__ = """List files with conflicts.
1765
1766@@ -127,7 +122,7 @@
1767 if all:
1768 if file_list:
1769 raise errors.CommandError(gettext("If --all is specified,"
1770- " no FILE may be provided"))
1771+ " no FILE may be provided"))
1772 if directory is None:
1773 directory = u'.'
1774 tree = workingtree.WorkingTree.open_containing(directory)[0]
1775@@ -182,14 +177,14 @@
1776 tree_conflicts = tree.conflicts()
1777 nb_conflicts_before = len(tree_conflicts)
1778 if paths is None:
1779- new_conflicts = ConflictList()
1780+ new_conflicts = []
1781 to_process = tree_conflicts
1782 else:
1783 new_conflicts, to_process = tree_conflicts.select_conflicts(
1784 tree, paths, ignore_misses, recursive)
1785 for conflict in to_process:
1786 try:
1787- conflict._do(action, tree)
1788+ conflict.do(action, tree)
1789 conflict.cleanup(tree)
1790 except NotImplementedError:
1791 new_conflicts.append(conflict)
1792@@ -235,8 +230,6 @@
1793 """List of conflicts.
1794
1795 Typically obtained from WorkingTree.conflicts()
1796-
1797- Can be instantiated from stanzas or from Conflict subclasses.
1798 """
1799
1800 def __init__(self, conflicts=None):
1801@@ -270,19 +263,6 @@
1802 def __repr__(self):
1803 return "ConflictList(%r)" % self.__list
1804
1805- @staticmethod
1806- def from_stanzas(stanzas):
1807- """Produce a new ConflictList from an iterable of stanzas"""
1808- conflicts = ConflictList()
1809- for stanza in stanzas:
1810- conflicts.append(Conflict.factory(**stanza.as_dict()))
1811- return conflicts
1812-
1813- def to_stanzas(self):
1814- """Generator of stanzas"""
1815- for conflict in self:
1816- yield conflict.as_stanza()
1817-
1818 def to_strings(self):
1819 """Generate strings for the provided conflicts"""
1820 for conflict in self:
1821@@ -299,43 +279,23 @@
1822 recurse=False):
1823 """Select the conflicts associated with paths in a tree.
1824
1825- File-ids are also used for this.
1826 :return: a pair of ConflictLists: (not_selected, selected)
1827 """
1828 path_set = set(paths)
1829- ids = {}
1830 selected_paths = set()
1831 new_conflicts = ConflictList()
1832 selected_conflicts = ConflictList()
1833- for path in paths:
1834- file_id = tree.path2id(path)
1835- if file_id is not None:
1836- ids[file_id] = path
1837
1838 for conflict in self:
1839 selected = False
1840- for key in ('path', 'conflict_path'):
1841- cpath = getattr(conflict, key, None)
1842- if cpath is None:
1843- continue
1844- if cpath in path_set:
1845+ if conflict.path in path_set:
1846+ selected = True
1847+ selected_paths.add(conflict.path)
1848+ if recurse:
1849+ if osutils.is_inside_any(path_set, conflict.path):
1850 selected = True
1851- selected_paths.add(cpath)
1852- if recurse:
1853- if osutils.is_inside_any(path_set, cpath):
1854- selected = True
1855- selected_paths.add(cpath)
1856+ selected_paths.add(conflict.path)
1857
1858- for key in ('file_id', 'conflict_file_id'):
1859- cfile_id = getattr(conflict, key, None)
1860- if cfile_id is None:
1861- continue
1862- try:
1863- cpath = ids[cfile_id]
1864- except KeyError:
1865- continue
1866- selected = True
1867- selected_paths.add(cpath)
1868 if selected:
1869 selected_conflicts.append(conflict)
1870 else:
1871@@ -350,84 +310,12 @@
1872
1873
1874 class Conflict(object):
1875- """Base class for all types of conflict"""
1876-
1877- # FIXME: cleanup should take care of that ? -- vila 091229
1878- has_files = False
1879-
1880- def __init__(self, path, file_id=None):
1881+ """Base class for conflicts."""
1882+
1883+ typestring = None
1884+
1885+ def __init__(self, path):
1886 self.path = path
1887- # the factory blindly transfers the Stanza values to __init__ and
1888- # Stanza is purely a Unicode api.
1889- if isinstance(file_id, str):
1890- file_id = cache_utf8.encode(file_id)
1891- self.file_id = file_id
1892-
1893- def as_stanza(self):
1894- s = rio.Stanza(type=self.typestring, path=self.path)
1895- if self.file_id is not None:
1896- # Stanza requires Unicode apis
1897- s.add('file_id', self.file_id.decode('utf8'))
1898- return s
1899-
1900- def _cmp_list(self):
1901- return [type(self), self.path, self.file_id]
1902-
1903- def __cmp__(self, other):
1904- if getattr(other, "_cmp_list", None) is None:
1905- return -1
1906- x = self._cmp_list()
1907- y = other._cmp_list()
1908- return (x > y) - (x < y)
1909-
1910- def __hash__(self):
1911- return hash((type(self), self.path, self.file_id))
1912-
1913- def __eq__(self, other):
1914- return self.__cmp__(other) == 0
1915-
1916- def __ne__(self, other):
1917- return not self.__eq__(other)
1918-
1919- def __unicode__(self):
1920- return self.describe()
1921-
1922- def __str__(self):
1923- return self.describe()
1924-
1925- def describe(self):
1926- return self.format % self.__dict__
1927-
1928- def __repr__(self):
1929- rdict = dict(self.__dict__)
1930- rdict['class'] = self.__class__.__name__
1931- return self.rformat % rdict
1932-
1933- @staticmethod
1934- def factory(type, **kwargs):
1935- global ctype
1936- return ctype[type](**kwargs)
1937-
1938- @staticmethod
1939- def sort_key(conflict):
1940- if conflict.path is not None:
1941- return conflict.path, conflict.typestring
1942- elif getattr(conflict, "conflict_path", None) is not None:
1943- return conflict.conflict_path, conflict.typestring
1944- else:
1945- return None, conflict.typestring
1946-
1947- def _do(self, action, tree):
1948- """Apply the specified action to the conflict.
1949-
1950- :param action: The method name to call.
1951-
1952- :param tree: The tree passed as a parameter to the method.
1953- """
1954- meth = getattr(self, 'action_%s' % action, None)
1955- if meth is None:
1956- raise NotImplementedError(self.__class__.__name__ + '.' + action)
1957- meth(tree)
1958
1959 def associated_filenames(self):
1960 """The names of the files generated to help resolve the conflict."""
1961@@ -441,472 +329,15 @@
1962 if e.errno != errno.ENOENT:
1963 raise
1964
1965- def action_auto(self, tree):
1966- raise NotImplementedError(self.action_auto)
1967-
1968- def action_done(self, tree):
1969- """Mark the conflict as solved once it has been handled."""
1970- # This method does nothing but simplifies the design of upper levels.
1971- pass
1972-
1973- def action_take_this(self, tree):
1974- raise NotImplementedError(self.action_take_this)
1975-
1976- def action_take_other(self, tree):
1977- raise NotImplementedError(self.action_take_other)
1978-
1979- def _resolve_with_cleanups(self, tree, *args, **kwargs):
1980- with tree.transform() as tt:
1981- self._resolve(tt, *args, **kwargs)
1982-
1983-
1984-class PathConflict(Conflict):
1985- """A conflict was encountered merging file paths"""
1986-
1987- typestring = 'path conflict'
1988-
1989- format = 'Path conflict: %(path)s / %(conflict_path)s'
1990-
1991- rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
1992-
1993- def __init__(self, path, conflict_path=None, file_id=None):
1994- Conflict.__init__(self, path, file_id)
1995- self.conflict_path = conflict_path
1996-
1997- def as_stanza(self):
1998- s = Conflict.as_stanza(self)
1999- if self.conflict_path is not None:
2000- s.add('conflict_path', self.conflict_path)
2001- return s
2002-
2003- def associated_filenames(self):
2004- # No additional files have been generated here
2005- return []
2006-
2007- def _resolve(self, tt, file_id, path, winner):
2008- """Resolve the conflict.
2009-
2010- :param tt: The TreeTransform where the conflict is resolved.
2011- :param file_id: The retained file id.
2012- :param path: The retained path.
2013- :param winner: 'this' or 'other' indicates which side is the winner.
2014- """
2015- path_to_create = None
2016- if winner == 'this':
2017- if self.path == '<deleted>':
2018- return # Nothing to do
2019- if self.conflict_path == '<deleted>':
2020- path_to_create = self.path
2021- revid = tt._tree.get_parent_ids()[0]
2022- elif winner == 'other':
2023- if self.conflict_path == '<deleted>':
2024- return # Nothing to do
2025- if self.path == '<deleted>':
2026- path_to_create = self.conflict_path
2027- # FIXME: If there are more than two parents we may need to
2028- # iterate. Taking the last parent is the safer bet in the mean
2029- # time. -- vila 20100309
2030- revid = tt._tree.get_parent_ids()[-1]
2031- else:
2032- # Programmer error
2033- raise AssertionError('bad winner: %r' % (winner,))
2034- if path_to_create is not None:
2035- tid = tt.trans_id_tree_path(path_to_create)
2036- tree = self._revision_tree(tt._tree, revid)
2037- transform.create_from_tree(
2038- tt, tid, tree, tree.id2path(file_id))
2039- tt.version_file(tid, file_id=file_id)
2040- else:
2041- tid = tt.trans_id_file_id(file_id)
2042- # Adjust the path for the retained file id
2043- parent_tid = tt.get_tree_parent(tid)
2044- tt.adjust_path(osutils.basename(path), parent_tid, tid)
2045- tt.apply()
2046-
2047- def _revision_tree(self, tree, revid):
2048- return tree.branch.repository.revision_tree(revid)
2049-
2050- def _infer_file_id(self, tree):
2051- # Prior to bug #531967, file_id wasn't always set, there may still be
2052- # conflict files in the wild so we need to cope with them
2053- # Establish which path we should use to find back the file-id
2054- possible_paths = []
2055- for p in (self.path, self.conflict_path):
2056- if p == '<deleted>':
2057- # special hard-coded path
2058- continue
2059- if p is not None:
2060- possible_paths.append(p)
2061- # Search the file-id in the parents with any path available
2062- file_id = None
2063- for revid in tree.get_parent_ids():
2064- revtree = self._revision_tree(tree, revid)
2065- for p in possible_paths:
2066- file_id = revtree.path2id(p)
2067- if file_id is not None:
2068- return revtree, file_id
2069- return None, None
2070-
2071- def action_take_this(self, tree):
2072- if self.file_id is not None:
2073- self._resolve_with_cleanups(tree, self.file_id, self.path,
2074- winner='this')
2075- else:
2076- # Prior to bug #531967 we need to find back the file_id and restore
2077- # the content from there
2078- revtree, file_id = self._infer_file_id(tree)
2079- tree.revert([revtree.id2path(file_id)],
2080- old_tree=revtree, backups=False)
2081-
2082- def action_take_other(self, tree):
2083- if self.file_id is not None:
2084- self._resolve_with_cleanups(tree, self.file_id,
2085- self.conflict_path,
2086- winner='other')
2087- else:
2088- # Prior to bug #531967 we need to find back the file_id and restore
2089- # the content from there
2090- revtree, file_id = self._infer_file_id(tree)
2091- tree.revert([revtree.id2path(file_id)],
2092- old_tree=revtree, backups=False)
2093-
2094-
2095-class ContentsConflict(PathConflict):
2096- """The files are of different types (or both binary), or not present"""
2097-
2098- has_files = True
2099-
2100- typestring = 'contents conflict'
2101-
2102- format = 'Contents conflict in %(path)s'
2103-
2104- def associated_filenames(self):
2105- return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
2106-
2107- def _resolve(self, tt, suffix_to_remove):
2108- """Resolve the conflict.
2109-
2110- :param tt: The TreeTransform where the conflict is resolved.
2111- :param suffix_to_remove: Either 'THIS' or 'OTHER'
2112-
2113- The resolution is symmetric: when taking THIS, OTHER is deleted and
2114- item.THIS is renamed into item and vice-versa.
2115- """
2116- try:
2117- # Delete 'item.THIS' or 'item.OTHER' depending on
2118- # suffix_to_remove
2119- tt.delete_contents(
2120- tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
2121- except errors.NoSuchFile:
2122- # There are valid cases where 'item.suffix_to_remove' either
2123- # never existed or was already deleted (including the case
2124- # where the user deleted it)
2125- pass
2126- try:
2127- this_path = tt._tree.id2path(self.file_id)
2128- except errors.NoSuchId:
2129- # The file is not present anymore. This may happen if the user
2130- # deleted the file either manually or when resolving a conflict on
2131- # the parent. We may raise some exception to indicate that the
2132- # conflict doesn't exist anymore and as such doesn't need to be
2133- # resolved ? -- vila 20110615
2134- this_tid = None
2135- else:
2136- this_tid = tt.trans_id_tree_path(this_path)
2137- if this_tid is not None:
2138- # Rename 'item.suffix_to_remove' (note that if
2139- # 'item.suffix_to_remove' has been deleted, this is a no-op)
2140- parent_tid = tt.get_tree_parent(this_tid)
2141- tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
2142- tt.apply()
2143-
2144- def action_take_this(self, tree):
2145- self._resolve_with_cleanups(tree, 'OTHER')
2146-
2147- def action_take_other(self, tree):
2148- self._resolve_with_cleanups(tree, 'THIS')
2149-
2150-
2151-# TODO: There should be a base revid attribute to better inform the user about
2152-# how the conflicts were generated.
2153-class TextConflict(Conflict):
2154- """The merge algorithm could not resolve all differences encountered."""
2155-
2156- has_files = True
2157-
2158- typestring = 'text conflict'
2159-
2160- format = 'Text conflict in %(path)s'
2161-
2162- rformat = '%(class)s(%(path)r, %(file_id)r)'
2163-
2164- _conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
2165-
2166- def associated_filenames(self):
2167- return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
2168-
2169- def _resolve(self, tt, winner_suffix):
2170- """Resolve the conflict by copying one of .THIS or .OTHER into file.
2171-
2172- :param tt: The TreeTransform where the conflict is resolved.
2173- :param winner_suffix: Either 'THIS' or 'OTHER'
2174-
2175- The resolution is symmetric, when taking THIS, item.THIS is renamed
2176- into item and vice-versa. This takes one of the files as a whole
2177- ignoring every difference that could have been merged cleanly.
2178- """
2179- # To avoid useless copies, we switch item and item.winner_suffix, only
2180- # item will exist after the conflict has been resolved anyway.
2181- item_tid = tt.trans_id_file_id(self.file_id)
2182- item_parent_tid = tt.get_tree_parent(item_tid)
2183- winner_path = self.path + '.' + winner_suffix
2184- winner_tid = tt.trans_id_tree_path(winner_path)
2185- winner_parent_tid = tt.get_tree_parent(winner_tid)
2186- # Switch the paths to preserve the content
2187- tt.adjust_path(osutils.basename(self.path),
2188- winner_parent_tid, winner_tid)
2189- tt.adjust_path(osutils.basename(winner_path),
2190- item_parent_tid, item_tid)
2191- # Associate the file_id to the right content
2192- tt.unversion_file(item_tid)
2193- tt.version_file(winner_tid, file_id=self.file_id)
2194- tt.apply()
2195-
2196- def action_auto(self, tree):
2197- # GZ 2012-07-27: Using NotImplementedError to signal that a conflict
2198- # can't be auto resolved does not seem ideal.
2199- try:
2200- kind = tree.kind(self.path)
2201- except errors.NoSuchFile:
2202- return
2203- if kind != 'file':
2204- raise NotImplementedError("Conflict is not a file")
2205- conflict_markers_in_line = self._conflict_re.search
2206- # GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
2207- with tree.get_file(self.path) as f:
2208- for line in f:
2209- if conflict_markers_in_line(line):
2210- raise NotImplementedError("Conflict markers present")
2211-
2212- def action_take_this(self, tree):
2213- self._resolve_with_cleanups(tree, 'THIS')
2214-
2215- def action_take_other(self, tree):
2216- self._resolve_with_cleanups(tree, 'OTHER')
2217-
2218-
2219-class HandledConflict(Conflict):
2220- """A path problem that has been provisionally resolved.
2221- This is intended to be a base class.
2222- """
2223-
2224- rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
2225-
2226- def __init__(self, action, path, file_id=None):
2227- Conflict.__init__(self, path, file_id)
2228- self.action = action
2229-
2230- def _cmp_list(self):
2231- return Conflict._cmp_list(self) + [self.action]
2232-
2233- def as_stanza(self):
2234- s = Conflict.as_stanza(self)
2235- s.add('action', self.action)
2236- return s
2237-
2238- def associated_filenames(self):
2239- # Nothing has been generated here
2240- return []
2241-
2242-
2243-class HandledPathConflict(HandledConflict):
2244- """A provisionally-resolved path problem involving two paths.
2245- This is intended to be a base class.
2246- """
2247-
2248- rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
2249- " %(file_id)r, %(conflict_file_id)r)"
2250-
2251- def __init__(self, action, path, conflict_path, file_id=None,
2252- conflict_file_id=None):
2253- HandledConflict.__init__(self, action, path, file_id)
2254- self.conflict_path = conflict_path
2255- # the factory blindly transfers the Stanza values to __init__,
2256- # so they can be unicode.
2257- if isinstance(conflict_file_id, str):
2258- conflict_file_id = cache_utf8.encode(conflict_file_id)
2259- self.conflict_file_id = conflict_file_id
2260-
2261- def _cmp_list(self):
2262- return HandledConflict._cmp_list(self) + [self.conflict_path,
2263- self.conflict_file_id]
2264-
2265- def as_stanza(self):
2266- s = HandledConflict.as_stanza(self)
2267- s.add('conflict_path', self.conflict_path)
2268- if self.conflict_file_id is not None:
2269- s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
2270-
2271- return s
2272-
2273-
2274-class DuplicateID(HandledPathConflict):
2275- """Two files want the same file_id."""
2276-
2277- typestring = 'duplicate id'
2278-
2279- format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
2280-
2281-
2282-class DuplicateEntry(HandledPathConflict):
2283- """Two directory entries want to have the same name."""
2284-
2285- typestring = 'duplicate'
2286-
2287- format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
2288-
2289- def action_take_this(self, tree):
2290- tree.remove([self.conflict_path], force=True, keep_files=False)
2291- tree.rename_one(self.path, self.conflict_path)
2292-
2293- def action_take_other(self, tree):
2294- tree.remove([self.path], force=True, keep_files=False)
2295-
2296-
2297-class ParentLoop(HandledPathConflict):
2298- """An attempt to create an infinitely-looping directory structure.
2299- This is rare, but can be produced like so:
2300-
2301- tree A:
2302- mv foo bar
2303- tree B:
2304- mv bar foo
2305- merge A and B
2306- """
2307-
2308- typestring = 'parent loop'
2309-
2310- format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
2311-
2312- def action_take_this(self, tree):
2313- # just acccept brz proposal
2314- pass
2315-
2316- def action_take_other(self, tree):
2317- with tree.transform() as tt:
2318- p_tid = tt.trans_id_file_id(self.file_id)
2319- parent_tid = tt.get_tree_parent(p_tid)
2320- cp_tid = tt.trans_id_file_id(self.conflict_file_id)
2321- cparent_tid = tt.get_tree_parent(cp_tid)
2322- tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
2323- tt.adjust_path(osutils.basename(self.conflict_path),
2324- parent_tid, p_tid)
2325- tt.apply()
2326-
2327-
2328-class UnversionedParent(HandledConflict):
2329- """An attempt to version a file whose parent directory is not versioned.
2330- Typically, the result of a merge where one tree unversioned the directory
2331- and the other added a versioned file to it.
2332- """
2333-
2334- typestring = 'unversioned parent'
2335-
2336- format = 'Conflict because %(path)s is not versioned, but has versioned'\
2337- ' children. %(action)s.'
2338-
2339- # FIXME: We silently do nothing to make tests pass, but most probably the
2340- # conflict shouldn't exist (the long story is that the conflict is
2341- # generated with another one that can be resolved properly) -- vila 091224
2342- def action_take_this(self, tree):
2343- pass
2344-
2345- def action_take_other(self, tree):
2346- pass
2347-
2348-
2349-class MissingParent(HandledConflict):
2350- """An attempt to add files to a directory that is not present.
2351- Typically, the result of a merge where THIS deleted the directory and
2352- the OTHER added a file to it.
2353- See also: DeletingParent (same situation, THIS and OTHER reversed)
2354- """
2355-
2356- typestring = 'missing parent'
2357-
2358- format = 'Conflict adding files to %(path)s. %(action)s.'
2359-
2360- def action_take_this(self, tree):
2361- tree.remove([self.path], force=True, keep_files=False)
2362-
2363- def action_take_other(self, tree):
2364- # just acccept brz proposal
2365- pass
2366-
2367-
2368-class DeletingParent(HandledConflict):
2369- """An attempt to add files to a directory that is not present.
2370- Typically, the result of a merge where one OTHER deleted the directory and
2371- the THIS added a file to it.
2372- """
2373-
2374- typestring = 'deleting parent'
2375-
2376- format = "Conflict: can't delete %(path)s because it is not empty. "\
2377- "%(action)s."
2378-
2379- # FIXME: It's a bit strange that the default action is not coherent with
2380- # MissingParent from the *user* pov.
2381-
2382- def action_take_this(self, tree):
2383- # just acccept brz proposal
2384- pass
2385-
2386- def action_take_other(self, tree):
2387- tree.remove([self.path], force=True, keep_files=False)
2388-
2389-
2390-class NonDirectoryParent(HandledConflict):
2391- """An attempt to add files to a directory that is not a directory or
2392- an attempt to change the kind of a directory with files.
2393- """
2394-
2395- typestring = 'non-directory parent'
2396-
2397- format = "Conflict: %(path)s is not a directory, but has files in it."\
2398- " %(action)s."
2399-
2400- # FIXME: .OTHER should be used instead of .new when the conflict is created
2401-
2402- def action_take_this(self, tree):
2403- # FIXME: we should preserve that path when the conflict is generated !
2404- if self.path.endswith('.new'):
2405- conflict_path = self.path[:-(len('.new'))]
2406- tree.remove([self.path], force=True, keep_files=False)
2407- tree.add(conflict_path)
2408- else:
2409- raise NotImplementedError(self.action_take_this)
2410-
2411- def action_take_other(self, tree):
2412- # FIXME: we should preserve that path when the conflict is generated !
2413- if self.path.endswith('.new'):
2414- conflict_path = self.path[:-(len('.new'))]
2415- tree.remove([conflict_path], force=True, keep_files=False)
2416- tree.rename_one(self.path, conflict_path)
2417- else:
2418- raise NotImplementedError(self.action_take_other)
2419-
2420-
2421-ctype = {}
2422-
2423-
2424-def register_types(*conflict_types):
2425- """Register a Conflict subclass for serialization purposes"""
2426- global ctype
2427- for conflict_type in conflict_types:
2428- ctype[conflict_type.typestring] = conflict_type
2429-
2430-
2431-register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
2432- DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
2433- DeletingParent, NonDirectoryParent)
2434+ def do(self, action, tree):
2435+ """Apply the specified action to the conflict.
2436+
2437+ :param action: The method name to call.
2438+
2439+ :param tree: The tree passed as a parameter to the method.
2440+ """
2441+ raise NotImplementedError(self.do)
2442+
2443+ def describe(self):
2444+ """Return a string description of this conflict."""
2445+ raise NotImplementedError(self.describe)
2446
2447=== modified file 'breezy/delta.py'
2448--- breezy/delta.py 2020-02-18 01:57:45 +0000
2449+++ breezy/delta.py 2020-08-23 01:05:31 +0000
2450@@ -20,7 +20,7 @@
2451 osutils,
2452 trace,
2453 )
2454-from .tree import TreeChange
2455+from .bzr.inventorytree import InventoryTreeChange
2456
2457
2458 class TreeDelta(object):
2459@@ -327,14 +327,12 @@
2460 exe_change = False
2461 # files are "renamed" if they are moved or if name changes, as long
2462 # as it had a value
2463- if None not in change.name and None not in change.parent_id and\
2464- (change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]):
2465- if change.copied:
2466- copied = True
2467- renamed = False
2468- else:
2469- renamed = True
2470- copied = False
2471+ if change.copied:
2472+ copied = True
2473+ renamed = False
2474+ elif change.renamed:
2475+ renamed = True
2476+ copied = False
2477 else:
2478 copied = False
2479 renamed = False
2480@@ -397,7 +395,7 @@
2481 dec_new_path = decorate_path(item.path[1], item.kind[1], item.meta_modified())
2482 to_file.write(' => %s' % dec_new_path)
2483 if item.changed_content or item.meta_modified():
2484- extra_modified.append(TreeChange(
2485+ extra_modified.append(InventoryTreeChange(
2486 item.file_id, (item.path[1], item.path[1]),
2487 item.changed_content,
2488 item.versioned,
2489
2490=== modified file 'breezy/errors.py'
2491--- breezy/errors.py 2020-07-28 02:11:05 +0000
2492+++ breezy/errors.py 2020-08-23 01:05:31 +0000
2493@@ -1282,15 +1282,15 @@
2494
2495 class UnexpectedHttpStatus(InvalidHttpResponse):
2496
2497- _fmt = "Unexpected HTTP status %(code)d for %(path)s"
2498+ _fmt = "Unexpected HTTP status %(code)d for %(path)s: %(extra)s"
2499
2500- def __init__(self, path, code, msg=None):
2501+ def __init__(self, path, code, extra=None):
2502 self.path = path
2503 self.code = code
2504- self.msg = msg
2505+ self.extra = extra or ''
2506 full_msg = 'status code %d unexpected' % code
2507- if msg is not None:
2508- full_msg += ': ' + msg
2509+ if extra is not None:
2510+ full_msg += ': ' + extra
2511 InvalidHttpResponse.__init__(
2512 self, path, full_msg)
2513
2514
2515=== modified file 'breezy/git/commit.py'
2516--- breezy/git/commit.py 2020-07-18 23:14:00 +0000
2517+++ breezy/git/commit.py 2020-08-23 01:05:31 +0000
2518@@ -19,6 +19,7 @@
2519
2520 from dulwich.index import (
2521 commit_tree,
2522+ read_submodule_head,
2523 )
2524 import stat
2525
2526@@ -42,7 +43,6 @@
2527 Blob,
2528 Commit,
2529 )
2530-from dulwich.index import read_submodule_head
2531
2532
2533 from .mapping import (
2534@@ -75,11 +75,24 @@
2535 def record_iter_changes(self, workingtree, basis_revid, iter_changes):
2536 seen_root = False
2537 for change in iter_changes:
2538+ if change.kind == (None, None):
2539+ # Ephemeral
2540+ continue
2541+ if change.versioned[0] and not change.copied:
2542+ file_id = self._mapping.generate_file_id(change.path[0])
2543+ elif change.versioned[1]:
2544+ file_id = self._mapping.generate_file_id(change.path[1])
2545+ else:
2546+ file_id = None
2547+ if change.path[1]:
2548+ parent_id_new = self._mapping.generate_file_id(osutils.dirname(change.path[1]))
2549+ else:
2550+ parent_id_new = None
2551 if change.kind[1] in ("directory",):
2552 self._inv_delta.append(
2553- (change.path[0], change.path[1], change.file_id,
2554+ (change.path[0], change.path[1], file_id,
2555 entry_factory[change.kind[1]](
2556- change.file_id, change.name[1], change.parent_id[1])))
2557+ file_id, change.name[1], parent_id_new)))
2558 if change.kind[0] in ("file", "symlink"):
2559 self._blobs[encode_git_path(change.path[0])] = None
2560 self._any_changes = True
2561@@ -88,14 +101,14 @@
2562 continue
2563 self._any_changes = True
2564 if change.path[1] is None:
2565- self._inv_delta.append((change.path[0], change.path[1], change.file_id, None))
2566+ self._inv_delta.append((change.path[0], change.path[1], file_id, None))
2567 self._deleted_paths.add(encode_git_path(change.path[0]))
2568 continue
2569 try:
2570 entry_kls = entry_factory[change.kind[1]]
2571 except KeyError:
2572 raise KeyError("unknown kind %s" % change.kind[1])
2573- entry = entry_kls(change.file_id, change.name[1], change.parent_id[1])
2574+ entry = entry_kls(file_id, change.name[1], parent_id_new)
2575 if change.kind[1] == "file":
2576 entry.executable = change.executable[1]
2577 blob = Blob()
2578@@ -104,10 +117,13 @@
2579 blob.data = f.read()
2580 finally:
2581 f.close()
2582- entry.text_size = len(blob.data)
2583- entry.text_sha1 = osutils.sha_string(blob.data)
2584+ sha = blob.id
2585+ if st is not None:
2586+ entry.text_size = st.st_size
2587+ else:
2588+ entry.text_size = len(blob.data)
2589+ entry.git_sha1 = sha
2590 self.store.add_object(blob)
2591- sha = blob.id
2592 elif change.kind[1] == "symlink":
2593 symlink_target = workingtree.get_symlink_target(change.path[1])
2594 blob = Blob()
2595@@ -124,12 +140,12 @@
2596 else:
2597 raise AssertionError("Unknown kind %r" % change.kind[1])
2598 mode = object_mode(change.kind[1], change.executable[1])
2599- self._inv_delta.append((change.path[0], change.path[1], change.file_id, entry))
2600+ self._inv_delta.append((change.path[0], change.path[1], file_id, entry))
2601 if change.path[0] is not None:
2602 self._deleted_paths.add(encode_git_path(change.path[0]))
2603 self._blobs[encode_git_path(change.path[1])] = (mode, sha)
2604 if st is not None:
2605- yield change.path[1], (entry.text_sha1, st)
2606+ yield change.path[1], (entry.git_sha1, st)
2607 if not seen_root and len(self.parents) == 0:
2608 raise RootMissing()
2609 if getattr(workingtree, "basis_tree", False):
2610
2611=== modified file 'breezy/git/mapping.py'
2612--- breezy/git/mapping.py 2020-07-18 23:14:00 +0000
2613+++ breezy/git/mapping.py 2020-08-23 01:05:31 +0000
2614@@ -160,7 +160,7 @@
2615 """Class that maps between Git and Bazaar semantics."""
2616 experimental = False
2617
2618- BZR_DUMMY_FILE = None
2619+ BZR_DUMMY_FILE = None # type: Optional[str]
2620
2621 def is_special_file(self, filename):
2622 return (filename in (self.BZR_DUMMY_FILE, ))
2623
2624=== modified file 'breezy/git/remote.py'
2625--- breezy/git/remote.py 2020-07-18 23:14:00 +0000
2626+++ breezy/git/remote.py 2020-08-23 01:05:31 +0000
2627@@ -201,6 +201,8 @@
2628 return PermissionDenied(url, message)
2629 if message.endswith(' does not appear to be a git repository'):
2630 return NotBranchError(url, message)
2631+ if message == 'pre-receive hook declined':
2632+ return PermissionDenied(url, message)
2633 if re.match('(.+) is not a valid repository name',
2634 message.splitlines()[0]):
2635 return NotBranchError(url, message)
2636@@ -600,11 +602,16 @@
2637 push_result.branch_push_result = None
2638 repo = self.find_repository()
2639 refname = self._get_selected_ref(name)
2640- ref_chain, old_sha = self.get_refs_container().follow(refname)
2641- if ref_chain:
2642- actual_refname = ref_chain[-1]
2643- else:
2644+ try:
2645+ ref_chain, old_sha = self.get_refs_container().follow(refname)
2646+ except NotBranchError:
2647 actual_refname = refname
2648+ old_sha = None
2649+ else:
2650+ if ref_chain:
2651+ actual_refname = ref_chain[-1]
2652+ else:
2653+ actual_refname = refname
2654 if isinstance(source, GitBranch) and lossy:
2655 raise errors.LossyPushToSameVCS(source.controldir, self)
2656 source_store = get_object_store(source.repository)
2657@@ -624,6 +631,7 @@
2658 raise errors.NoRoundtrippingSupport(
2659 source, self.open_branch(name=name, nascent_ok=True))
2660 if not overwrite:
2661+ old_sha = remote_refs.get(actual_refname)
2662 if remote_divergence(old_sha, new_sha, source_store):
2663 raise DivergedBranches(
2664 source, self.open_branch(name, nascent_ok=True))
2665
2666=== modified file 'breezy/git/tests/test_remote.py'
2667--- breezy/git/tests/test_remote.py 2020-07-18 23:14:00 +0000
2668+++ breezy/git/tests/test_remote.py 2020-08-23 01:05:31 +0000
2669@@ -135,6 +135,14 @@
2670 self.assertEqual(e.path, 'porridge/gaduhistory.git')
2671 self.assertEqual(e.extra, ': denied to jelmer')
2672
2673+ def test_pre_receive_hook_declined(self):
2674+ e = parse_git_error(
2675+ "url",
2676+ 'pre-receive hook declined')
2677+ self.assertIsInstance(e, PermissionDenied)
2678+ self.assertEqual(e.path, "url")
2679+ self.assertEqual(e.extra, ': pre-receive hook declined')
2680+
2681 def test_invalid_repo_name(self):
2682 e = parse_git_error(
2683 "url",
2684
2685=== modified file 'breezy/git/tests/test_tree.py'
2686--- breezy/git/tests/test_tree.py 2020-06-21 22:00:40 +0000
2687+++ breezy/git/tests/test_tree.py 2020-08-23 01:05:31 +0000
2688@@ -25,7 +25,7 @@
2689 from dulwich.objects import Tree, Blob
2690
2691 from breezy.delta import TreeDelta
2692-from breezy.tree import TreeChange
2693+from breezy.bzr.inventorytree import InventoryTreeChange as TreeChange
2694 from breezy.git.tree import (
2695 changes_from_git_changes,
2696 tree_delta_from_git_changes,
2697
2698=== modified file 'breezy/git/tests/test_workingtree.py'
2699--- breezy/git/tests/test_workingtree.py 2020-06-19 21:26:53 +0000
2700+++ breezy/git/tests/test_workingtree.py 2020-08-23 01:05:31 +0000
2701@@ -21,8 +21,11 @@
2702 import stat
2703
2704 from dulwich import __version__ as dulwich_version
2705-from dulwich.diff_tree import RenameDetector
2706+from dulwich.diff_tree import RenameDetector, tree_changes
2707 from dulwich.index import IndexEntry
2708+from dulwich.object_store import (
2709+ OverlayObjectStore,
2710+ )
2711 from dulwich.objects import (
2712 S_IFGITLINK,
2713 Blob,
2714@@ -35,12 +38,11 @@
2715 workingtree as _mod_workingtree,
2716 )
2717 from ...delta import TreeDelta
2718-from ...tree import TreeChange
2719+from ...bzr.inventorytree import InventoryTreeChange as TreeChange
2720 from ..mapping import (
2721 default_mapping,
2722 )
2723 from ..tree import (
2724- changes_between_git_tree_and_working_copy,
2725 tree_delta_from_git_changes,
2726 )
2727 from ..workingtree import (
2728@@ -52,6 +54,22 @@
2729 )
2730
2731
2732+def changes_between_git_tree_and_working_copy(source_store, from_tree_sha, target,
2733+ want_unchanged=False,
2734+ want_unversioned=False,
2735+ rename_detector=None,
2736+ include_trees=True):
2737+ """Determine the changes between a git tree and a working tree with index.
2738+
2739+ """
2740+ to_tree_sha, extras = target.git_snapshot(want_unversioned=want_unversioned)
2741+ store = OverlayObjectStore([source_store, target.store])
2742+ return tree_changes(
2743+ store, from_tree_sha, to_tree_sha, include_trees=include_trees,
2744+ rename_detector=rename_detector,
2745+ want_unchanged=want_unchanged, change_type_same=True), extras
2746+
2747+
2748 class GitWorkingTreeTests(TestCaseWithTransport):
2749
2750 def setUp(self):
2751
2752=== modified file 'breezy/git/transform.py'
2753--- breezy/git/transform.py 2020-07-20 02:02:23 +0000
2754+++ breezy/git/transform.py 2020-08-23 01:05:31 +0000
2755@@ -19,13 +19,29 @@
2756
2757 import errno
2758 import os
2759-from stat import S_ISREG
2760+import posixpath
2761+from stat import S_IEXEC, S_ISREG
2762 import time
2763
2764-from .. import errors, multiparent, osutils, trace, ui, urlutils
2765+from .mapping import encode_git_path, mode_kind, mode_is_executable, object_mode
2766+from .tree import GitTree, GitTreeDirectory, GitTreeSymlink, GitTreeFile
2767+
2768+from .. import (
2769+ annotate,
2770+ conflicts,
2771+ errors,
2772+ multiparent,
2773+ osutils,
2774+ revision as _mod_revision,
2775+ trace,
2776+ ui,
2777+ urlutils,
2778+ )
2779 from ..i18n import gettext
2780 from ..mutabletree import MutableTree
2781+from ..tree import InterTree, TreeChange
2782 from ..transform import (
2783+ PreviewTree,
2784 TreeTransform,
2785 _TransformResults,
2786 _FileMover,
2787@@ -39,8 +55,8 @@
2788 MalformedTransform,
2789 )
2790
2791-from ..bzr import inventory
2792-from ..bzr.transform import TransformPreview as GitTransformPreview
2793+from dulwich.index import commit_tree, blob_from_path_and_stat
2794+from dulwich.objects import Blob
2795
2796
2797 class TreeTransformBase(TreeTransform):
2798@@ -58,19 +74,17 @@
2799 super(TreeTransformBase, self).__init__(tree, pb=pb)
2800 # mapping of trans_id => (sha1 of content, stat_value)
2801 self._observed_sha1s = {}
2802- # Mapping of trans_id -> new file_id
2803- self._new_id = {}
2804- # Mapping of old file-id -> trans_id
2805- self._non_present_ids = {}
2806- # Mapping of new file_id -> trans_id
2807- self._r_new_id = {}
2808+ # Set of versioned trans ids
2809+ self._versioned = set()
2810 # The trans_id that will be used as the tree root
2811- if tree.is_versioned(''):
2812- self._new_root = self.trans_id_tree_path('')
2813- else:
2814- self._new_root = None
2815+ self.root = self.trans_id_tree_path('')
2816 # Whether the target is case sensitive
2817 self._case_sensitive_target = case_sensitive
2818+ self._symlink_target = {}
2819+
2820+ @property
2821+ def mapping(self):
2822+ return self._tree.mapping
2823
2824 def finalize(self):
2825 """Release the working tree lock, if held.
2826@@ -85,49 +99,16 @@
2827 self._tree.unlock()
2828 self._tree = None
2829
2830- def __get_root(self):
2831- return self._new_root
2832-
2833- root = property(__get_root)
2834-
2835 def create_path(self, name, parent):
2836 """Assign a transaction id to a new path"""
2837- trans_id = self._assign_id()
2838+ trans_id = self.assign_id()
2839 unique_add(self._new_name, trans_id, name)
2840 unique_add(self._new_parent, trans_id, parent)
2841 return trans_id
2842
2843 def adjust_root_path(self, name, parent):
2844 """Emulate moving the root by moving all children, instead.
2845-
2846- We do this by undoing the association of root's transaction id with the
2847- current tree. This allows us to create a new directory with that
2848- transaction id. We unversion the root directory and version the
2849- physically new directory, and hope someone versions the tree root
2850- later.
2851 """
2852- old_root = self._new_root
2853- old_root_file_id = self.final_file_id(old_root)
2854- # force moving all children of root
2855- for child_id in self.iter_tree_children(old_root):
2856- if child_id != parent:
2857- self.adjust_path(self.final_name(child_id),
2858- self.final_parent(child_id), child_id)
2859- file_id = self.final_file_id(child_id)
2860- if file_id is not None:
2861- self.unversion_file(child_id)
2862- self.version_file(child_id, file_id=file_id)
2863-
2864- # the physical root needs a new transaction id
2865- self._tree_path_ids.pop("")
2866- self._tree_id_paths.pop(old_root)
2867- self._new_root = self.trans_id_tree_path('')
2868- if parent == old_root:
2869- parent = self._new_root
2870- self.adjust_path(name, parent, old_root)
2871- self.create_directory(old_root)
2872- self.version_file(old_root, file_id=old_root_file_id)
2873- self.unversion_file(self._new_root)
2874
2875 def fixup_new_roots(self):
2876 """Reinterpret requests to change the root directory
2877@@ -147,26 +128,12 @@
2878 return
2879 if len(new_roots) != 1:
2880 raise ValueError('A tree cannot have two roots!')
2881- if self._new_root is None:
2882- self._new_root = new_roots[0]
2883- return
2884 old_new_root = new_roots[0]
2885 # unversion the new root's directory.
2886- if self.final_kind(self._new_root) is None:
2887- file_id = self.final_file_id(old_new_root)
2888- else:
2889- file_id = self.final_file_id(self._new_root)
2890- if old_new_root in self._new_id:
2891+ if old_new_root in self._versioned:
2892 self.cancel_versioning(old_new_root)
2893 else:
2894 self.unversion_file(old_new_root)
2895- # if, at this stage, root still has an old file_id, zap it so we can
2896- # stick a new one in.
2897- if (self.tree_file_id(self._new_root) is not None
2898- and self._new_root not in self._removed_id):
2899- self.unversion_file(self._new_root)
2900- if file_id is not None:
2901- self.version_file(self._new_root, file_id=file_id)
2902
2903 # Now move children of new root into old root directory.
2904 # Ensure all children are registered with the transaction, but don't
2905@@ -174,7 +141,7 @@
2906 list(self.iter_tree_children(old_new_root))
2907 # Move all children of new root into old root directory.
2908 for child in self.by_parent().get(old_new_root, []):
2909- self.adjust_path(self.final_name(child), self._new_root, child)
2910+ self.adjust_path(self.final_name(child), self.root, child)
2911
2912 # Ensure old_new_root has no directory.
2913 if old_new_root in self._new_contents:
2914@@ -183,8 +150,8 @@
2915 self.delete_contents(old_new_root)
2916
2917 # prevent deletion of root directory.
2918- if self._new_root in self._removed_contents:
2919- self.cancel_deletion(self._new_root)
2920+ if self.root in self._removed_contents:
2921+ self.cancel_deletion(self.root)
2922
2923 # destroy path info for old_new_root.
2924 del self._new_parent[old_new_root]
2925@@ -198,24 +165,14 @@
2926 """
2927 if file_id is None:
2928 raise ValueError('None is not a valid file id')
2929- if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
2930- return self._r_new_id[file_id]
2931- else:
2932- try:
2933- path = self._tree.id2path(file_id)
2934- except errors.NoSuchId:
2935- if file_id in self._non_present_ids:
2936- return self._non_present_ids[file_id]
2937- else:
2938- trans_id = self._assign_id()
2939- self._non_present_ids[file_id] = trans_id
2940- return trans_id
2941- else:
2942- return self.trans_id_tree_path(path)
2943+ path = self.mapping.parse_file_id(file_id)
2944+ return self.trans_id_tree_path(path)
2945
2946 def version_file(self, trans_id, file_id=None):
2947 """Schedule a file to become versioned."""
2948- raise NotImplementedError(self.version_file)
2949+ if trans_id in self._versioned:
2950+ raise errors.DuplicateKey(key=trans_id)
2951+ self._versioned.add(trans_id)
2952
2953 def cancel_versioning(self, trans_id):
2954 """Undo a previous versioning of a file"""
2955@@ -232,55 +189,27 @@
2956 stale_ids = self._needs_rename.difference(self._new_name)
2957 stale_ids.difference_update(self._new_parent)
2958 stale_ids.difference_update(self._new_contents)
2959- stale_ids.difference_update(self._new_id)
2960+ stale_ids.difference_update(self._versioned)
2961 needs_rename = self._needs_rename.difference(stale_ids)
2962 id_sets = (needs_rename, self._new_executability)
2963 else:
2964 id_sets = (self._new_name, self._new_parent, self._new_contents,
2965- self._new_id, self._new_executability)
2966+ self._versioned, self._new_executability)
2967 for id_set in id_sets:
2968 new_ids.update(id_set)
2969 return sorted(FinalPaths(self).get_paths(new_ids))
2970
2971- def tree_file_id(self, trans_id):
2972- """Determine the file id associated with the trans_id in the tree"""
2973- path = self.tree_path(trans_id)
2974- if path is None:
2975- return None
2976- # the file is old; the old id is still valid
2977- if self._new_root == trans_id:
2978- return self._tree.path2id('')
2979- return self._tree.path2id(path)
2980-
2981 def final_is_versioned(self, trans_id):
2982- return self.final_file_id(trans_id) is not None
2983-
2984- def final_file_id(self, trans_id):
2985- """Determine the file id after any changes are applied, or None.
2986-
2987- None indicates that the file will not be versioned after changes are
2988- applied.
2989- """
2990- try:
2991- return self._new_id[trans_id]
2992- except KeyError:
2993- if trans_id in self._removed_id:
2994- return None
2995- return self.tree_file_id(trans_id)
2996-
2997- def inactive_file_id(self, trans_id):
2998- """Return the inactive file_id associated with a transaction id.
2999- That is, the one in the tree or in non_present_ids.
3000- The file_id may actually be active, too.
3001- """
3002- file_id = self.tree_file_id(trans_id)
3003- if file_id is not None:
3004- return file_id
3005- for key, value in self._non_present_ids.items():
3006- if value == trans_id:
3007- return key
3008-
3009- def find_conflicts(self):
3010+ if trans_id in self._versioned:
3011+ return True
3012+ if trans_id in self._removed_id:
3013+ return False
3014+ orig_path = self.tree_path(trans_id)
3015+ if orig_path is None:
3016+ return False
3017+ return self._tree.is_versioned(orig_path)
3018+
3019+ def find_raw_conflicts(self):
3020 """Find any violations of inventory or filesystem invariants"""
3021 if self._done is True:
3022 raise ReusingTransform()
3023@@ -289,7 +218,6 @@
3024 # all children of non-existent parents are known, by definition.
3025 self._add_tree_children()
3026 by_parent = self.by_parent()
3027- conflicts.extend(self._unversioned_parents(by_parent))
3028 conflicts.extend(self._parent_loops())
3029 conflicts.extend(self._duplicate_entries(by_parent))
3030 conflicts.extend(self._parent_type_conflicts(by_parent))
3031@@ -299,7 +227,7 @@
3032 return conflicts
3033
3034 def _check_malformed(self):
3035- conflicts = self.find_conflicts()
3036+ conflicts = self.find_raw_conflicts()
3037 if len(conflicts) != 0:
3038 raise MalformedTransform(conflicts=conflicts)
3039
3040@@ -315,8 +243,11 @@
3041 for trans_id in self._removed_id:
3042 path = self.tree_path(trans_id)
3043 if path is not None:
3044- if self._tree.stored_kind(path) == 'directory':
3045- parents.append(trans_id)
3046+ try:
3047+ if self._tree.stored_kind(path) == 'directory':
3048+ parents.append(trans_id)
3049+ except errors.NoSuchFile:
3050+ pass
3051 elif self.tree_kind(trans_id) == 'directory':
3052 parents.append(trans_id)
3053
3054@@ -385,27 +316,13 @@
3055 break
3056 return conflicts
3057
3058- def _unversioned_parents(self, by_parent):
3059- """If parent directories are versioned, children must be versioned."""
3060- conflicts = []
3061- for parent_id, children in by_parent.items():
3062- if parent_id == ROOT_PARENT:
3063- continue
3064- if self.final_is_versioned(parent_id):
3065- continue
3066- for child_id in children:
3067- if self.final_is_versioned(child_id):
3068- conflicts.append(('unversioned parent', parent_id))
3069- break
3070- return conflicts
3071-
3072 def _improper_versioning(self):
3073 """Cannot version a file with no contents, or a bad type.
3074
3075 However, existing entries with no contents are okay.
3076 """
3077 conflicts = []
3078- for trans_id in self._new_id:
3079+ for trans_id in self._versioned:
3080 kind = self.final_kind(trans_id)
3081 if kind == 'symlink' and not self._tree.supports_symlinks():
3082 # Ignore symlinks as they are not supported on this platform
3083@@ -611,7 +528,7 @@
3084 def _affected_ids(self):
3085 """Return the set of transform ids affected by the transform"""
3086 trans_ids = set(self._removed_id)
3087- trans_ids.update(self._new_id)
3088+ trans_ids.update(self._versioned)
3089 trans_ids.update(self._removed_contents)
3090 trans_ids.update(self._new_contents)
3091 trans_ids.update(self._new_executability)
3092@@ -619,130 +536,78 @@
3093 trans_ids.update(self._new_parent)
3094 return trans_ids
3095
3096- def _get_file_id_maps(self):
3097- """Return mapping of file_ids to trans_ids in the to and from states"""
3098- trans_ids = self._affected_ids()
3099- from_trans_ids = {}
3100- to_trans_ids = {}
3101- # Build up two dicts: trans_ids associated with file ids in the
3102- # FROM state, vs the TO state.
3103- for trans_id in trans_ids:
3104- from_file_id = self.tree_file_id(trans_id)
3105- if from_file_id is not None:
3106- from_trans_ids[from_file_id] = trans_id
3107- to_file_id = self.final_file_id(trans_id)
3108- if to_file_id is not None:
3109- to_trans_ids[to_file_id] = trans_id
3110- return from_trans_ids, to_trans_ids
3111-
3112- def _from_file_data(self, from_trans_id, from_versioned, from_path):
3113- """Get data about a file in the from (tree) state
3114-
3115- Return a (name, parent, kind, executable) tuple
3116- """
3117- from_path = self._tree_id_paths.get(from_trans_id)
3118- if from_versioned:
3119- # get data from working tree if versioned
3120- from_entry = next(self._tree.iter_entries_by_dir(
3121- specific_files=[from_path]))[1]
3122- from_name = from_entry.name
3123- from_parent = from_entry.parent_id
3124- else:
3125- from_entry = None
3126- if from_path is None:
3127- # File does not exist in FROM state
3128- from_name = None
3129- from_parent = None
3130- else:
3131- # File exists, but is not versioned. Have to use path-
3132- # splitting stuff
3133- from_name = os.path.basename(from_path)
3134- tree_parent = self.get_tree_parent(from_trans_id)
3135- from_parent = self.tree_file_id(tree_parent)
3136- if from_path is not None:
3137- from_kind, from_executable, from_stats = \
3138- self._tree._comparison_data(from_entry, from_path)
3139- else:
3140- from_kind = None
3141- from_executable = False
3142- return from_name, from_parent, from_kind, from_executable
3143-
3144- def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
3145- """Get data about a file in the to (target) state
3146-
3147- Return a (name, parent, kind, executable) tuple
3148- """
3149- to_name = self.final_name(to_trans_id)
3150- to_kind = self.final_kind(to_trans_id)
3151- to_parent = self.final_file_id(self.final_parent(to_trans_id))
3152- if to_trans_id in self._new_executability:
3153- to_executable = self._new_executability[to_trans_id]
3154- elif to_trans_id == from_trans_id:
3155- to_executable = from_executable
3156- else:
3157- to_executable = False
3158- return to_name, to_parent, to_kind, to_executable
3159-
3160- def iter_changes(self):
3161+ def iter_changes(self, want_unversioned=False):
3162 """Produce output in the same format as Tree.iter_changes.
3163
3164 Will produce nonsensical results if invoked while inventory/filesystem
3165- conflicts (as reported by TreeTransform.find_conflicts()) are present.
3166-
3167- This reads the Transform, but only reproduces changes involving a
3168- file_id. Files that are not versioned in either of the FROM or TO
3169- states are not reflected.
3170+ conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
3171 """
3172 final_paths = FinalPaths(self)
3173- from_trans_ids, to_trans_ids = self._get_file_id_maps()
3174+ trans_ids = self._affected_ids()
3175 results = []
3176- # Now iterate through all active file_ids
3177- for file_id in set(from_trans_ids).union(to_trans_ids):
3178+ # Now iterate through all active paths
3179+ for trans_id in trans_ids:
3180+ from_path = self.tree_path(trans_id)
3181 modified = False
3182- from_trans_id = from_trans_ids.get(file_id)
3183 # find file ids, and determine versioning state
3184- if from_trans_id is None:
3185+ if from_path is None:
3186 from_versioned = False
3187- from_trans_id = to_trans_ids[file_id]
3188 else:
3189- from_versioned = True
3190- to_trans_id = to_trans_ids.get(file_id)
3191- if to_trans_id is None:
3192+ from_versioned = self._tree.is_versioned(from_path)
3193+ if not want_unversioned and not from_versioned:
3194+ from_path = None
3195+ to_path = final_paths.get_path(trans_id)
3196+ if to_path is None:
3197 to_versioned = False
3198- to_trans_id = from_trans_id
3199- else:
3200- to_versioned = True
3201-
3202- if not from_versioned:
3203- from_path = None
3204- else:
3205- from_path = self._tree_id_paths.get(from_trans_id)
3206- if not to_versioned:
3207+ else:
3208+ to_versioned = self.final_is_versioned(trans_id)
3209+ if not want_unversioned and not to_versioned:
3210 to_path = None
3211- else:
3212- to_path = final_paths.get_path(to_trans_id)
3213-
3214- from_name, from_parent, from_kind, from_executable = \
3215- self._from_file_data(from_trans_id, from_versioned, from_path)
3216-
3217- to_name, to_parent, to_kind, to_executable = \
3218- self._to_file_data(to_trans_id, from_trans_id, from_executable)
3219-
3220- if from_kind != to_kind:
3221+
3222+ if from_versioned:
3223+ # get data from working tree if versioned
3224+ from_entry = next(self._tree.iter_entries_by_dir(
3225+ specific_files=[from_path]))[1]
3226+ from_name = from_entry.name
3227+ else:
3228+ from_entry = None
3229+ if from_path is None:
3230+ # File does not exist in FROM state
3231+ from_name = None
3232+ else:
3233+ # File exists, but is not versioned. Have to use path-
3234+ # splitting stuff
3235+ from_name = os.path.basename(from_path)
3236+ if from_path is not None:
3237+ from_kind, from_executable, from_stats = \
3238+ self._tree._comparison_data(from_entry, from_path)
3239+ else:
3240+ from_kind = None
3241+ from_executable = False
3242+
3243+ to_name = self.final_name(trans_id)
3244+ to_kind = self.final_kind(trans_id)
3245+ if trans_id in self._new_executability:
3246+ to_executable = self._new_executability[trans_id]
3247+ else:
3248+ to_executable = from_executable
3249+
3250+ if from_versioned and from_kind != to_kind:
3251 modified = True
3252 elif to_kind in ('file', 'symlink') and (
3253- to_trans_id != from_trans_id
3254- or to_trans_id in self._new_contents):
3255+ trans_id in self._new_contents):
3256 modified = True
3257 if (not modified and from_versioned == to_versioned
3258- and from_parent == to_parent and from_name == to_name
3259+ and from_path == to_path
3260+ and from_name == to_name
3261 and from_executable == to_executable):
3262 continue
3263+ if (from_path, to_path) == (None, None):
3264+ continue
3265 results.append(
3266 TreeChange(
3267- file_id, (from_path, to_path), modified,
3268+ (from_path, to_path), modified,
3269 (from_versioned, to_versioned),
3270- (from_parent, to_parent),
3271 (from_name, to_name),
3272 (from_kind, to_kind),
3273 (from_executable, to_executable)))
3274@@ -757,7 +622,7 @@
3275 The tree is a snapshot, and altering the TreeTransform will invalidate
3276 it.
3277 """
3278- raise NotImplementedError(self.get_preview_tree)
3279+ return GitPreviewTree(self)
3280
3281 def commit(self, branch, message, merge_parents=None, strict=False,
3282 timestamp=None, timezone=None, committer=None, authors=None,
3283@@ -784,7 +649,7 @@
3284 """
3285 self._check_malformed()
3286 if strict:
3287- unversioned = set(self._new_contents).difference(set(self._new_id))
3288+ unversioned = set(self._new_contents).difference(set(self._versioned))
3289 for trans_id in unversioned:
3290 if not self.final_is_versioned(trans_id):
3291 raise errors.StrictCommitFailed()
3292@@ -841,104 +706,6 @@
3293 return ()
3294 return (self._tree.get_file_lines(path),)
3295
3296- def serialize(self, serializer):
3297- """Serialize this TreeTransform.
3298-
3299- :param serializer: A Serialiser like pack.ContainerSerializer.
3300- """
3301- from .. import bencode
3302- new_name = {k.encode('utf-8'): v.encode('utf-8')
3303- for k, v in self._new_name.items()}
3304- new_parent = {k.encode('utf-8'): v.encode('utf-8')
3305- for k, v in self._new_parent.items()}
3306- new_id = {k.encode('utf-8'): v
3307- for k, v in self._new_id.items()}
3308- new_executability = {k.encode('utf-8'): int(v)
3309- for k, v in self._new_executability.items()}
3310- tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
3311- for k, v in self._tree_path_ids.items()}
3312- non_present_ids = {k: v.encode('utf-8')
3313- for k, v in self._non_present_ids.items()}
3314- removed_contents = [trans_id.encode('utf-8')
3315- for trans_id in self._removed_contents]
3316- removed_id = [trans_id.encode('utf-8')
3317- for trans_id in self._removed_id]
3318- attribs = {
3319- b'_id_number': self._id_number,
3320- b'_new_name': new_name,
3321- b'_new_parent': new_parent,
3322- b'_new_executability': new_executability,
3323- b'_new_id': new_id,
3324- b'_tree_path_ids': tree_path_ids,
3325- b'_removed_id': removed_id,
3326- b'_removed_contents': removed_contents,
3327- b'_non_present_ids': non_present_ids,
3328- }
3329- yield serializer.bytes_record(bencode.bencode(attribs),
3330- ((b'attribs',),))
3331- for trans_id, kind in sorted(self._new_contents.items()):
3332- if kind == 'file':
3333- with open(self._limbo_name(trans_id), 'rb') as cur_file:
3334- lines = cur_file.readlines()
3335- parents = self._get_parents_lines(trans_id)
3336- mpdiff = multiparent.MultiParent.from_lines(lines, parents)
3337- content = b''.join(mpdiff.to_patch())
3338- if kind == 'directory':
3339- content = b''
3340- if kind == 'symlink':
3341- content = self._read_symlink_target(trans_id)
3342- if not isinstance(content, bytes):
3343- content = content.encode('utf-8')
3344- yield serializer.bytes_record(
3345- content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
3346-
3347- def deserialize(self, records):
3348- """Deserialize a stored TreeTransform.
3349-
3350- :param records: An iterable of (names, content) tuples, as per
3351- pack.ContainerPushParser.
3352- """
3353- from .. import bencode
3354- names, content = next(records)
3355- attribs = bencode.bdecode(content)
3356- self._id_number = attribs[b'_id_number']
3357- self._new_name = {k.decode('utf-8'): v.decode('utf-8')
3358- for k, v in attribs[b'_new_name'].items()}
3359- self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
3360- for k, v in attribs[b'_new_parent'].items()}
3361- self._new_executability = {
3362- k.decode('utf-8'): bool(v)
3363- for k, v in attribs[b'_new_executability'].items()}
3364- self._new_id = {k.decode('utf-8'): v
3365- for k, v in attribs[b'_new_id'].items()}
3366- self._r_new_id = {v: k for k, v in self._new_id.items()}
3367- self._tree_path_ids = {}
3368- self._tree_id_paths = {}
3369- for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
3370- path = bytepath.decode('utf-8')
3371- trans_id = trans_id.decode('utf-8')
3372- self._tree_path_ids[path] = trans_id
3373- self._tree_id_paths[trans_id] = path
3374- self._removed_id = {trans_id.decode('utf-8')
3375- for trans_id in attribs[b'_removed_id']}
3376- self._removed_contents = set(
3377- trans_id.decode('utf-8')
3378- for trans_id in attribs[b'_removed_contents'])
3379- self._non_present_ids = {
3380- k: v.decode('utf-8')
3381- for k, v in attribs[b'_non_present_ids'].items()}
3382- for ((trans_id, kind),), content in records:
3383- trans_id = trans_id.decode('utf-8')
3384- kind = kind.decode('ascii')
3385- if kind == 'file':
3386- mpdiff = multiparent.MultiParent.from_patch(content)
3387- lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
3388- self.create_file(lines, trans_id)
3389- if kind == 'directory':
3390- self.create_directory(trans_id)
3391- if kind == 'symlink':
3392- self.create_symlink(content.decode('utf-8'), trans_id)
3393-
3394 def create_file(self, contents, trans_id, mode_id=None, sha1=None):
3395 """Schedule creation of a new file.
3396
3397@@ -994,6 +761,35 @@
3398 """
3399 raise NotImplementedError(self.apply)
3400
3401+ def cook_conflicts(self, raw_conflicts):
3402+ """Generate a list of cooked conflicts, sorted by file path"""
3403+ if not raw_conflicts:
3404+ return
3405+ fp = FinalPaths(self)
3406+ from .workingtree import TextConflict
3407+ for c in raw_conflicts:
3408+ if c[0] == 'text conflict':
3409+ yield TextConflict(fp.get_path(c[1]))
3410+ elif c[0] == 'duplicate':
3411+ yield TextConflict(fp.get_path(c[2]))
3412+ elif c[0] == 'contents conflict':
3413+ yield TextConflict(fp.get_path(c[1][0]))
3414+ elif c[0] == 'missing parent':
3415+ # TODO(jelmer): This should not make it to here
3416+ yield TextConflict(fp.get_path(c[2]))
3417+ elif c[0] == 'non-directory parent':
3418+ yield TextConflict(fp.get_path(c[2]))
3419+ elif c[0] == 'deleting parent':
3420+ # TODO(jelmer): This should not make it to here
3421+ yield TextConflict(fp.get_path(c[2]))
3422+ elif c[0] == 'parent loop':
3423+ # TODO(jelmer): This should not make it to here
3424+ yield TextConflict(fp.get_path(c[2]))
3425+ elif c[0] == 'path conflict':
3426+ yield TextConflict(fp.get_path(c[1]))
3427+ else:
3428+ raise AssertionError('unknown conflict %s' % c[0])
3429+
3430
3431 class DiskTreeTransform(TreeTransformBase):
3432 """Tree transform storing its contents on disk."""
3433@@ -1204,6 +1000,7 @@
3434 path = None
3435 trace.warning(
3436 'Unable to create symlink "%s" on this filesystem.' % (path,))
3437+ self._symlink_target[trans_id] = target
3438 # We add symlink to _new_contents even if they are unsupported
3439 # and not created. These entries are subsequently used to avoid
3440 # conflicts on platforms that don't support symlink
3441@@ -1228,6 +1025,88 @@
3442 handle_orphan = conf.get('transform.orphan_policy')
3443 handle_orphan(self, trans_id, parent_id)
3444
3445+ def final_entry(self, trans_id):
3446+ is_versioned = self.final_is_versioned(trans_id)
3447+ fp = FinalPaths(self)
3448+ tree_path = fp.get_path(trans_id)
3449+ if trans_id in self._new_contents:
3450+ path = self._limbo_name(trans_id)
3451+ st = os.lstat(path)
3452+ kind = mode_kind(st.st_mode)
3453+ name = self.final_name(trans_id)
3454+ file_id = self._tree.mapping.generate_file_id(tree_path)
3455+ parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
3456+ if kind == 'directory':
3457+ return GitTreeDirectory(
3458+ file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
3459+ executable = mode_is_executable(st.st_mode)
3460+ mode = object_mode(kind, executable)
3461+ blob = blob_from_path_and_stat(encode_git_path(path), st)
3462+ if kind == 'symlink':
3463+ return GitTreeSymlink(
3464+ file_id, name, parent_id,
3465+ decode_git_path(blob.data)), is_versioned
3466+ elif kind == 'file':
3467+ return GitTreeFile(
3468+ file_id, name, executable=executable, parent_id=parent_id,
3469+ git_sha1=blob.id, text_size=len(blob.data)), is_versioned
3470+ else:
3471+ raise AssertionError(kind)
3472+ elif trans_id in self._removed_contents:
3473+ return None, None
3474+ else:
3475+ orig_path = self.tree_path(trans_id)
3476+ if orig_path is None:
3477+ return None, None
3478+ file_id = self._tree.mapping.generate_file_id(tree_path)
3479+ if tree_path == '':
3480+ parent_id = None
3481+ else:
3482+ parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
3483+ try:
3484+ ie = next(self._tree.iter_entries_by_dir(
3485+ specific_files=[orig_path]))[1]
3486+ ie.file_id = file_id
3487+ ie.parent_id = parent_id
3488+ return ie, is_versioned
3489+ except StopIteration:
3490+ try:
3491+ if self.tree_kind(trans_id) == 'directory':
3492+ return GitTreeDirectory(
3493+ file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
3494+ except OSError as e:
3495+ if e.errno != errno.ENOTDIR:
3496+ raise
3497+ return None, None
3498+
3499+ def final_git_entry(self, trans_id):
3500+ if trans_id in self._new_contents:
3501+ path = self._limbo_name(trans_id)
3502+ st = os.lstat(path)
3503+ kind = mode_kind(st.st_mode)
3504+ if kind == 'directory':
3505+ return None, None
3506+ executable = mode_is_executable(st.st_mode)
3507+ mode = object_mode(kind, executable)
3508+ blob = blob_from_path_and_stat(encode_git_path(path), st)
3509+ elif trans_id in self._removed_contents:
3510+ return None, None
3511+ else:
3512+ orig_path = self.tree_path(trans_id)
3513+ kind = self._tree.kind(orig_path)
3514+ executable = self._tree.is_executable(orig_path)
3515+ mode = object_mode(kind, executable)
3516+ if kind == 'symlink':
3517+ contents = self._tree.get_symlink_target(orig_path)
3518+ elif kind == 'file':
3519+ contents = self._tree.get_file_text(orig_path)
3520+ elif kind == 'directory':
3521+ return None, None
3522+ else:
3523+ raise AssertionError(kind)
3524+ blob = Blob.from_string(contents)
3525+ return blob, mode
3526+
3527
3528 class GitTreeTransform(DiskTreeTransform):
3529 """Represent a tree transformation.
3530@@ -1448,20 +1327,11 @@
3531 self._limbo_children_names[parent][filename] = trans_id
3532 return limbo_name
3533
3534- def version_file(self, trans_id, file_id=None):
3535- """Schedule a file to become versioned."""
3536- if file_id is None:
3537- raise ValueError()
3538- unique_add(self._new_id, trans_id, file_id)
3539- unique_add(self._r_new_id, file_id, trans_id)
3540-
3541 def cancel_versioning(self, trans_id):
3542 """Undo a previous versioning of a file"""
3543- file_id = self._new_id[trans_id]
3544- del self._new_id[trans_id]
3545- del self._r_new_id[file_id]
3546+ self._versioned.remove(trans_id)
3547
3548- def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
3549+ def apply(self, no_conflicts=False, _mover=None):
3550 """Apply all changes to the inventory and filesystem.
3551
3552 If filesystem or inventory conflicts are present, MalformedTransform
3553@@ -1471,8 +1341,6 @@
3554
3555 :param no_conflicts: if True, the caller guarantees there are no
3556 conflicts, so no check is made.
3557- :param precomputed_delta: An inventory delta to use instead of
3558- calculating one.
3559 :param _mover: Supply an alternate FileMover, for testing
3560 """
3561 for hook in MutableTree.hooks['pre_transform']:
3562@@ -1481,14 +1349,9 @@
3563 self._check_malformed()
3564 self.rename_count = 0
3565 with ui.ui_factory.nested_progress_bar() as child_pb:
3566- if precomputed_delta is None:
3567- child_pb.update(gettext('Apply phase'), 0, 2)
3568- changes = self._generate_transform_changes()
3569- offset = 1
3570- else:
3571- changes = [
3572- (op, np, ie) for (op, np, fid, ie) in precomputed_delta]
3573- offset = 0
3574+ child_pb.update(gettext('Apply phase'), 0, 2)
3575+ index_changes = self._generate_index_changes()
3576+ offset = 1
3577 if _mover is None:
3578 mover = _FileMover()
3579 else:
3580@@ -1503,9 +1366,7 @@
3581 raise
3582 else:
3583 mover.apply_deletions()
3584- if self.final_file_id(self.root) is None:
3585- changes = [e for e in changes if e[0] != '']
3586- self._tree._apply_transform_delta(changes)
3587+ self._tree._apply_index_changes(index_changes)
3588 self._done = True
3589 self.finalize()
3590 return _TransformResults(modified_paths, self.rename_count)
3591@@ -1588,97 +1449,354 @@
3592 self._new_contents.clear()
3593 return modified_paths
3594
3595- def _inventory_altered(self):
3596- """Determine which trans_ids need new Inventory entries.
3597-
3598- An new entry is needed when anything that would be reflected by an
3599- inventory entry changes, including file name, file_id, parent file_id,
3600- file kind, and the execute bit.
3601-
3602- Some care is taken to return entries with real changes, not cases
3603- where the value is deleted and then restored to its original value,
3604- but some actually unchanged values may be returned.
3605-
3606- :returns: A list of (path, trans_id) for all items requiring an
3607- inventory change. Ordered by path.
3608- """
3609+ def _generate_index_changes(self):
3610+ """Generate an inventory delta for the current transform."""
3611+ removed_id = set(self._removed_id)
3612+ removed_id.update(self._removed_contents)
3613+ changes = {}
3614 changed_ids = set()
3615- # Find entries whose file_ids are new (or changed).
3616- new_file_id = set(t for t in self._new_id
3617- if self._new_id[t] != self.tree_file_id(t))
3618- for id_set in [self._new_name, self._new_parent, new_file_id,
3619+ for id_set in [self._new_name, self._new_parent,
3620 self._new_executability]:
3621 changed_ids.update(id_set)
3622- # removing implies a kind change
3623- changed_kind = set(self._removed_contents)
3624+ for id_set in [self._new_name, self._new_parent]:
3625+ removed_id.update(id_set)
3626 # so does adding
3627- changed_kind.intersection_update(self._new_contents)
3628+ changed_kind = set(self._new_contents)
3629 # Ignore entries that are already known to have changed.
3630 changed_kind.difference_update(changed_ids)
3631 # to keep only the truly changed ones
3632 changed_kind = (t for t in changed_kind
3633 if self.tree_kind(t) != self.final_kind(t))
3634- # all kind changes will alter the inventory
3635 changed_ids.update(changed_kind)
3636- # To find entries with changed parent_ids, find parents which existed,
3637- # but changed file_id.
3638- # Now add all their children to the set.
3639- for parent_trans_id in new_file_id:
3640- changed_ids.update(self.iter_tree_children(parent_trans_id))
3641- return sorted(FinalPaths(self).get_paths(changed_ids))
3642-
3643- def _generate_transform_changes(self):
3644- """Generate an inventory delta for the current transform."""
3645- changes = []
3646- new_paths = self._inventory_altered()
3647- total_entries = len(new_paths) + len(self._removed_id)
3648+ for t in changed_kind:
3649+ if self.final_kind(t) == 'directory':
3650+ removed_id.add(t)
3651+ changed_ids.remove(t)
3652+ new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
3653+ total_entries = len(new_paths) + len(removed_id)
3654 with ui.ui_factory.nested_progress_bar() as child_pb:
3655- for num, trans_id in enumerate(self._removed_id):
3656+ for num, trans_id in enumerate(removed_id):
3657 if (num % 10) == 0:
3658 child_pb.update(gettext('removing file'),
3659 num, total_entries)
3660- if trans_id == self._new_root:
3661- file_id = self._tree.path2id('')
3662- else:
3663- file_id = self.tree_file_id(trans_id)
3664- # File-id isn't really being deleted, just moved
3665- if file_id in self._r_new_id:
3666+ try:
3667+ path = self._tree_id_paths[trans_id]
3668+ except KeyError:
3669 continue
3670- path = self._tree_id_paths[trans_id]
3671- changes.append((path, None, None))
3672- new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
3673- new_paths)
3674+ changes[path] = (None, None, None, None)
3675 for num, (path, trans_id) in enumerate(new_paths):
3676 if (num % 10) == 0:
3677 child_pb.update(gettext('adding file'),
3678- num + len(self._removed_id), total_entries)
3679- file_id = new_path_file_ids[trans_id]
3680- if file_id is None:
3681- continue
3682+ num + len(removed_id), total_entries)
3683+
3684 kind = self.final_kind(trans_id)
3685 if kind is None:
3686- kind = self._tree.stored_kind(self._tree.id2path(file_id))
3687- parent_trans_id = self.final_parent(trans_id)
3688- parent_file_id = new_path_file_ids.get(parent_trans_id)
3689- if parent_file_id is None:
3690- parent_file_id = self.final_file_id(parent_trans_id)
3691- if trans_id in self._new_reference_revision:
3692- new_entry = inventory.TreeReference(
3693- file_id,
3694- self._new_name[trans_id],
3695- self.final_file_id(self._new_parent[trans_id]),
3696- None, self._new_reference_revision[trans_id])
3697- else:
3698- new_entry = inventory.make_entry(kind,
3699- self.final_name(trans_id),
3700- parent_file_id, file_id)
3701- try:
3702- old_path = self._tree.id2path(new_entry.file_id)
3703- except errors.NoSuchId:
3704- old_path = None
3705- new_executability = self._new_executability.get(trans_id)
3706- if new_executability is not None:
3707- new_entry.executable = new_executability
3708- changes.append(
3709- (old_path, path, new_entry))
3710- return changes
3711+ continue
3712+ versioned = self.final_is_versioned(trans_id)
3713+ if not versioned:
3714+ continue
3715+ executability = self._new_executability.get(trans_id)
3716+ reference_revision = self._new_reference_revision.get(trans_id)
3717+ symlink_target = self._symlink_target.get(trans_id)
3718+ changes[path] = (
3719+ kind, executability, reference_revision, symlink_target)
3720+ return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
3721+
3722+
3723+class GitTransformPreview(GitTreeTransform):
3724+ """A TreeTransform for generating preview trees.
3725+
3726+ Unlike TreeTransform, this version works when the input tree is a
3727+ RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
3728+ unversioned files in the input tree.
3729+ """
3730+
3731+ def __init__(self, tree, pb=None, case_sensitive=True):
3732+ tree.lock_read()
3733+ limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
3734+ DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
3735+
3736+ def canonical_path(self, path):
3737+ return path
3738+
3739+ def tree_kind(self, trans_id):
3740+ path = self.tree_path(trans_id)
3741+ if path is None:
3742+ return None
3743+ kind = self._tree.path_content_summary(path)[0]
3744+ if kind == 'missing':
3745+ kind = None
3746+ return kind
3747+
3748+ def _set_mode(self, trans_id, mode_id, typefunc):
3749+ """Set the mode of new file contents.
3750+ The mode_id is the existing file to get the mode from (often the same
3751+ as trans_id). The operation is only performed if there's a mode match
3752+ according to typefunc.
3753+ """
3754+ # is it ok to ignore this? probably
3755+ pass
3756+
3757+ def iter_tree_children(self, parent_id):
3758+ """Iterate through the entry's tree children, if any"""
3759+ try:
3760+ path = self._tree_id_paths[parent_id]
3761+ except KeyError:
3762+ return
3763+ try:
3764+ for child in self._tree.iter_child_entries(path):
3765+ childpath = joinpath(path, child.name)
3766+ yield self.trans_id_tree_path(childpath)
3767+ except errors.NoSuchFile:
3768+ return
3769+
3770+ def new_orphan(self, trans_id, parent_id):
3771+ raise NotImplementedError(self.new_orphan)
3772+
3773+
3774+class GitPreviewTree(PreviewTree, GitTree):
3775+ """Partial implementation of Tree to support show_diff_trees"""
3776+
3777+ def __init__(self, transform):
3778+ PreviewTree.__init__(self, transform)
3779+ self.store = transform._tree.store
3780+ self.mapping = transform._tree.mapping
3781+ self._final_paths = FinalPaths(transform)
3782+
3783+ def supports_setting_file_ids(self):
3784+ return False
3785+
3786+ def _supports_executable(self):
3787+ return self._transform._limbo_supports_executable()
3788+
3789+ def walkdirs(self, prefix=''):
3790+ pending = [self._transform.root]
3791+ while len(pending) > 0:
3792+ parent_id = pending.pop()
3793+ children = []
3794+ subdirs = []
3795+ prefix = prefix.rstrip('/')
3796+ parent_path = self._final_paths.get_path(parent_id)
3797+ for child_id in self._all_children(parent_id):
3798+ path_from_root = self._final_paths.get_path(child_id)
3799+ basename = self._transform.final_name(child_id)
3800+ kind = self._transform.final_kind(child_id)
3801+ if kind is not None:
3802+ versioned_kind = kind
3803+ else:
3804+ kind = 'unknown'
3805+ versioned_kind = self._transform._tree.stored_kind(
3806+ path_from_root)
3807+ if versioned_kind == 'directory':
3808+ subdirs.append(child_id)
3809+ children.append((path_from_root, basename, kind, None,
3810+ versioned_kind))
3811+ children.sort()
3812+ if parent_path.startswith(prefix):
3813+ yield parent_path, children
3814+ pending.extend(sorted(subdirs, key=self._final_paths.get_path,
3815+ reverse=True))
3816+
3817+ def iter_changes(self, from_tree, include_unchanged=False,
3818+ specific_files=None, pb=None, extra_trees=None,
3819+ require_versioned=True, want_unversioned=False):
3820+ """See InterTree.iter_changes.
3821+
3822+ This has a fast path that is only used when the from_tree matches
3823+ the transform tree, and no fancy options are supplied.
3824+ """
3825+ return InterTree.get(from_tree, self).iter_changes(
3826+ include_unchanged=include_unchanged,
3827+ specific_files=specific_files,
3828+ pb=pb,
3829+ extra_trees=extra_trees,
3830+ require_versioned=require_versioned,
3831+ want_unversioned=want_unversioned)
3832+
3833+ def get_file(self, path):
3834+ """See Tree.get_file"""
3835+ trans_id = self._path2trans_id(path)
3836+ if trans_id is None:
3837+ raise errors.NoSuchFile(path)
3838+ if trans_id in self._transform._new_contents:
3839+ name = self._transform._limbo_name(trans_id)
3840+ return open(name, 'rb')
3841+ if trans_id in self._transform._removed_contents:
3842+ raise errors.NoSuchFile(path)
3843+ orig_path = self._transform.tree_path(trans_id)
3844+ return self._transform._tree.get_file(orig_path)
3845+
3846+ def get_symlink_target(self, path):
3847+ """See Tree.get_symlink_target"""
3848+ trans_id = self._path2trans_id(path)
3849+ if trans_id is None:
3850+ raise errors.NoSuchFile(path)
3851+ if trans_id not in self._transform._new_contents:
3852+ orig_path = self._transform.tree_path(trans_id)
3853+ return self._transform._tree.get_symlink_target(orig_path)
3854+ name = self._transform._limbo_name(trans_id)
3855+ return osutils.readlink(name)
3856+
3857+ def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
3858+ trans_id = self._path2trans_id(path)
3859+ if trans_id is None:
3860+ return None
3861+ orig_path = self._transform.tree_path(trans_id)
3862+ if orig_path is not None:
3863+ old_annotation = self._transform._tree.annotate_iter(
3864+ orig_path, default_revision=default_revision)
3865+ else:
3866+ old_annotation = []
3867+ try:
3868+ lines = self.get_file_lines(path)
3869+ except errors.NoSuchFile:
3870+ return None
3871+ return annotate.reannotate([old_annotation], lines, default_revision)
3872+
3873+ def get_file_text(self, path):
3874+ """Return the byte content of a file.
3875+
3876+ :param path: The path of the file.
3877+
3878+ :returns: A single byte string for the whole file.
3879+ """
3880+ with self.get_file(path) as my_file:
3881+ return my_file.read()
3882+
3883+ def get_file_lines(self, path):
3884+ """Return the content of a file, as lines.
3885+
3886+ :param path: The path of the file.
3887+ """
3888+ return osutils.split_lines(self.get_file_text(path))
3889+
3890+ def extras(self):
3891+ possible_extras = set(self._transform.trans_id_tree_path(p) for p
3892+ in self._transform._tree.extras())
3893+ possible_extras.update(self._transform._new_contents)
3894+ possible_extras.update(self._transform._removed_id)
3895+ for trans_id in possible_extras:
3896+ if not self._transform.final_is_versioned(trans_id):
3897+ yield self._final_paths._determine_path(trans_id)
3898+
3899+ def path_content_summary(self, path):
3900+ trans_id = self._path2trans_id(path)
3901+ tt = self._transform
3902+ tree_path = tt.tree_path(trans_id)
3903+ kind = tt._new_contents.get(trans_id)
3904+ if kind is None:
3905+ if tree_path is None or trans_id in tt._removed_contents:
3906+ return 'missing', None, None, None
3907+ summary = tt._tree.path_content_summary(tree_path)
3908+ kind, size, executable, link_or_sha1 = summary
3909+ else:
3910+ link_or_sha1 = None
3911+ limbo_name = tt._limbo_name(trans_id)
3912+ if trans_id in tt._new_reference_revision:
3913+ kind = 'tree-reference'
3914+ if kind == 'file':
3915+ statval = os.lstat(limbo_name)
3916+ size = statval.st_size
3917+ if not tt._limbo_supports_executable():
3918+ executable = False
3919+ else:
3920+ executable = statval.st_mode & S_IEXEC
3921+ else:
3922+ size = None
3923+ executable = None
3924+ if kind == 'symlink':
3925+ link_or_sha1 = os.readlink(limbo_name)
3926+ if not isinstance(link_or_sha1, str):
3927+ link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
3928+ executable = tt._new_executability.get(trans_id, executable)
3929+ return kind, size, executable, link_or_sha1
3930+
3931+ def get_file_mtime(self, path):
3932+ """See Tree.get_file_mtime"""
3933+ trans_id = self._path2trans_id(path)
3934+ if trans_id is None:
3935+ raise errors.NoSuchFile(path)
3936+ if trans_id not in self._transform._new_contents:
3937+ return self._transform._tree.get_file_mtime(
3938+ self._transform.tree_path(trans_id))
3939+ name = self._transform._limbo_name(trans_id)
3940+ statval = os.lstat(name)
3941+ return statval.st_mtime
3942+
3943+ def is_versioned(self, path):
3944+ trans_id = self._path2trans_id(path)
3945+ if trans_id is None:
3946+ # It doesn't exist, so it's not versioned.
3947+ return False
3948+ if trans_id in self._transform._versioned:
3949+ return True
3950+ if trans_id in self._transform._removed_id:
3951+ return False
3952+ orig_path = self._transform.tree_path(trans_id)
3953+ return self._transform._tree.is_versioned(orig_path)
3954+
3955+ def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
3956+ if recurse_nested:
3957+ raise NotImplementedError(
3958+ 'follow tree references not yet supported')
3959+
3960+ # This may not be a maximally efficient implementation, but it is
3961+ # reasonably straightforward. An implementation that grafts the
3962+ # TreeTransform changes onto the tree's iter_entries_by_dir results
3963+ # might be more efficient, but requires tricky inferences about stack
3964+ # position.
3965+ for trans_id, path in self._list_files_by_dir():
3966+ entry, is_versioned = self._transform.final_entry(trans_id)
3967+ if entry is None:
3968+ continue
3969+ if not is_versioned and entry.kind != 'directory':
3970+ continue
3971+ if specific_files is not None and path not in specific_files:
3972+ continue
3973+ if entry is not None:
3974+ yield path, entry
3975+
3976+ def _list_files_by_dir(self):
3977+ todo = [ROOT_PARENT]
3978+ while len(todo) > 0:
3979+ parent = todo.pop()
3980+ children = list(self._all_children(parent))
3981+ paths = dict(zip(children, self._final_paths.get_paths(children)))
3982+ children.sort(key=paths.get)
3983+ todo.extend(reversed(children))
3984+ for trans_id in children:
3985+ yield trans_id, paths[trans_id][0]
3986+
3987+ def revision_tree(self, revision_id):
3988+ return self._transform._tree.revision_tree(revision_id)
3989+
3990+ def _stat_limbo_file(self, trans_id):
3991+ name = self._transform._limbo_name(trans_id)
3992+ return os.lstat(name)
3993+
3994+ def git_snapshot(self, want_unversioned=False):
3995+ extra = set()
3996+ os = []
3997+ for trans_id, path in self._list_files_by_dir():
3998+ if not self._transform.final_is_versioned(trans_id):
3999+ if not want_unversioned:
4000+ continue
4001+ extra.add(path)
4002+ o, mode = self._transform.final_git_entry(trans_id)
4003+ if o is not None:
4004+ self.store.add_object(o)
4005+ os.append((encode_git_path(path), o.id, mode))
4006+ if not os:
4007+ return None, extra
4008+ return commit_tree(self.store, os), extra
4009+
4010+ def iter_child_entries(self, path):
4011+ trans_id = self._path2trans_id(path)
4012+ if trans_id is None:
4013+ raise errors.NoSuchFile(path)
4014+ for child_trans_id in self._all_children(trans_id):
4015+ entry, is_versioned = self._transform.final_entry(trans_id)
4016+ if not is_versioned:
4017+ continue
4018+ if entry is not None:
4019+ yield entry
4020
4021=== modified file 'breezy/git/tree.py'
4022--- breezy/git/tree.py 2020-07-18 23:14:00 +0000
4023+++ breezy/git/tree.py 2020-08-23 01:05:31 +0000
4024@@ -76,18 +76,17 @@
4025 TransportObjectStore,
4026 TransportRepo,
4027 )
4028+from ..bzr.inventorytree import InventoryTreeChange
4029
4030
4031 class GitTreeDirectory(_mod_tree.TreeDirectory):
4032
4033- __slots__ = ['file_id', 'name', 'parent_id', 'children']
4034+ __slots__ = ['file_id', 'name', 'parent_id']
4035
4036 def __init__(self, file_id, name, parent_id):
4037 self.file_id = file_id
4038 self.name = name
4039 self.parent_id = parent_id
4040- # TODO(jelmer)
4041- self.children = {}
4042
4043 @property
4044 def kind(self):
4045@@ -115,16 +114,16 @@
4046
4047 class GitTreeFile(_mod_tree.TreeFile):
4048
4049- __slots__ = ['file_id', 'name', 'parent_id', 'text_size', 'text_sha1',
4050- 'executable']
4051+ __slots__ = ['file_id', 'name', 'parent_id', 'text_size',
4052+ 'executable', 'git_sha1']
4053
4054 def __init__(self, file_id, name, parent_id, text_size=None,
4055- text_sha1=None, executable=None):
4056+ git_sha1=None, executable=None):
4057 self.file_id = file_id
4058 self.name = name
4059 self.parent_id = parent_id
4060 self.text_size = text_size
4061- self.text_sha1 = text_sha1
4062+ self.git_sha1 = git_sha1
4063 self.executable = executable
4064
4065 @property
4066@@ -136,20 +135,20 @@
4067 self.file_id == other.file_id and
4068 self.name == other.name and
4069 self.parent_id == other.parent_id and
4070- self.text_sha1 == other.text_sha1 and
4071+ self.git_sha1 == other.git_sha1 and
4072 self.text_size == other.text_size and
4073 self.executable == other.executable)
4074
4075 def __repr__(self):
4076 return ("%s(file_id=%r, name=%r, parent_id=%r, text_size=%r, "
4077- "text_sha1=%r, executable=%r)") % (
4078+ "git_sha1=%r, executable=%r)") % (
4079 type(self).__name__, self.file_id, self.name, self.parent_id,
4080- self.text_size, self.text_sha1, self.executable)
4081+ self.text_size, self.git_sha1, self.executable)
4082
4083 def copy(self):
4084 ret = self.__class__(
4085 self.file_id, self.name, self.parent_id)
4086- ret.text_sha1 = self.text_sha1
4087+ ret.git_sha1 = self.git_sha1
4088 ret.text_size = self.text_size
4089 ret.executable = self.executable
4090 return ret
4091@@ -257,7 +256,57 @@
4092 return path
4093
4094
4095-class GitRevisionTree(revisiontree.RevisionTree):
4096+class GitTree(_mod_tree.Tree):
4097+
4098+ def iter_git_objects(self):
4099+ """Iterate over all the objects in the tree.
4100+
4101+ :return :Yields tuples with (path, sha, mode)
4102+ """
4103+ raise NotImplementedError(self.iter_git_objects)
4104+
4105+ def git_snapshot(self, want_unversioned=False):
4106+ """Snapshot a tree, and return tree object.
4107+
4108+ :return: Tree sha and set of extras
4109+ """
4110+ raise NotImplementedError(self.snapshot)
4111+
4112+ def preview_transform(self, pb=None):
4113+ from .transform import GitTransformPreview
4114+ return GitTransformPreview(self, pb=pb)
4115+
4116+ def find_related_paths_across_trees(self, paths, trees=[],
4117+ require_versioned=True):
4118+ if paths is None:
4119+ return None
4120+ if require_versioned:
4121+ trees = [self] + (trees if trees is not None else [])
4122+ unversioned = set()
4123+ for p in paths:
4124+ for t in trees:
4125+ if t.is_versioned(p):
4126+ break
4127+ else:
4128+ unversioned.add(p)
4129+ if unversioned:
4130+ raise errors.PathsNotVersionedError(unversioned)
4131+ return filter(self.is_versioned, paths)
4132+
4133+ def _submodule_info(self):
4134+ if self._submodules is None:
4135+ try:
4136+ with self.get_file('.gitmodules') as f:
4137+ config = GitConfigFile.from_file(f)
4138+ self._submodules = {
4139+ path: (url, section)
4140+ for path, url, section in parse_submodules(config)}
4141+ except errors.NoSuchFile:
4142+ self._submodules = {}
4143+ return self._submodules
4144+
4145+
4146+class GitRevisionTree(revisiontree.RevisionTree, GitTree):
4147 """Revision tree implementation based on Git objects."""
4148
4149 def __init__(self, repository, revision_id):
4150@@ -279,17 +328,8 @@
4151 raise errors.NoSuchRevision(repository, revision_id)
4152 self.tree = commit.tree
4153
4154- def _submodule_info(self):
4155- if self._submodules is None:
4156- try:
4157- with self.get_file('.gitmodules') as f:
4158- config = GitConfigFile.from_file(f)
4159- self._submodules = {
4160- path: (url, section)
4161- for path, url, section in parse_submodules(config)}
4162- except errors.NoSuchFile:
4163- self._submodules = {}
4164- return self._submodules
4165+ def git_snapshot(self, want_unversioned=False):
4166+ return self.tree, set()
4167
4168 def _get_submodule_repository(self, relpath):
4169 if not isinstance(relpath, bytes):
4170@@ -421,18 +461,6 @@
4171 else:
4172 return True
4173
4174- def _submodule_info(self):
4175- if self._submodules is None:
4176- try:
4177- with self.get_file('.gitmodules') as f:
4178- config = GitConfigFile.from_file(f)
4179- self._submodules = {
4180- path: (url, section)
4181- for path, url, section in parse_submodules(config)}
4182- except errors.NoSuchFile:
4183- self._submodules = {}
4184- return self._submodules
4185-
4186 def list_files(self, include_root=False, from_dir=None, recursive=True,
4187 recurse_nested=False):
4188 if self.tree is None:
4189@@ -496,9 +524,8 @@
4190 ie.reference_revision = self.mapping.revision_id_foreign_to_bzr(
4191 hexsha)
4192 else:
4193- data = store[hexsha].data
4194- ie.text_sha1 = osutils.sha_string(data)
4195- ie.text_size = len(data)
4196+ ie.git_sha1 = hexsha
4197+ ie.text_size = None
4198 ie.executable = mode_is_executable(mode)
4199 return ie
4200
4201@@ -649,23 +676,6 @@
4202 else:
4203 return (kind, None, None, None)
4204
4205- def find_related_paths_across_trees(self, paths, trees=[],
4206- require_versioned=True):
4207- if paths is None:
4208- return None
4209- if require_versioned:
4210- trees = [self] + (trees if trees is not None else [])
4211- unversioned = set()
4212- for p in paths:
4213- for t in trees:
4214- if t.is_versioned(p):
4215- break
4216- else:
4217- unversioned.add(p)
4218- if unversioned:
4219- raise errors.PathsNotVersionedError(unversioned)
4220- return filter(self.is_versioned, paths)
4221-
4222 def _iter_tree_contents(self, include_trees=False):
4223 if self.tree is None:
4224 return iter([])
4225@@ -699,9 +709,9 @@
4226 def walkdirs(self, prefix=u""):
4227 (store, mode, hexsha) = self._lookup_path(prefix)
4228 todo = deque(
4229- [(store, encode_git_path(prefix), hexsha, self.path2id(prefix))])
4230+ [(store, encode_git_path(prefix), hexsha)])
4231 while todo:
4232- store, path, tree_sha, parent_id = todo.popleft()
4233+ store, path, tree_sha = todo.popleft()
4234 path_decoded = decode_git_path(path)
4235 tree = store[tree_sha]
4236 children = []
4237@@ -709,18 +719,13 @@
4238 if self.mapping.is_special_file(name):
4239 continue
4240 child_path = posixpath.join(path, name)
4241- file_id = self.path2id(decode_git_path(child_path))
4242 if stat.S_ISDIR(mode):
4243- todo.append((store, child_path, hexsha, file_id))
4244+ todo.append((store, child_path, hexsha))
4245 children.append(
4246 (decode_git_path(child_path), decode_git_path(name),
4247 mode_kind(mode), None,
4248- file_id, mode_kind(mode)))
4249- yield (path_decoded, parent_id), children
4250-
4251- def preview_transform(self, pb=None):
4252- from .transform import GitTransformPreview
4253- return GitTransformPreview(self, pb=pb)
4254+ mode_kind(mode)))
4255+ yield path_decoded, children
4256
4257
4258 def tree_delta_from_git_changes(changes, mappings,
4259@@ -814,7 +819,7 @@
4260 newpath = None
4261 if oldpath is None and newpath is None:
4262 continue
4263- change = _mod_tree.TreeChange(
4264+ change = InventoryTreeChange(
4265 fileid, (oldpath_decoded, newpath_decoded), (oldsha != newsha),
4266 (oldversioned, newversioned),
4267 (oldparent, newparent), (oldname, newname),
4268@@ -860,7 +865,7 @@
4269 parent_id = new_mapping.generate_file_id(parent_path)
4270 file_id = new_mapping.generate_file_id(path_decoded)
4271 ret.added.append(
4272- _mod_tree.TreeChange(
4273+ InventoryTreeChange(
4274 file_id, (None, path_decoded), True,
4275 (False, True),
4276 (None, parent_id),
4277@@ -956,8 +961,13 @@
4278 fileid = mapping.generate_file_id(newpath_decoded)
4279 else:
4280 fileid = None
4281- yield _mod_tree.TreeChange(
4282- fileid, (oldpath_decoded, newpath_decoded), (oldsha != newsha),
4283+ if oldkind == 'directory' and newkind == 'directory':
4284+ modified = False
4285+ else:
4286+ modified = (oldsha != newsha) or (oldmode != newmode)
4287+ yield InventoryTreeChange(
4288+ fileid, (oldpath_decoded, newpath_decoded),
4289+ modified,
4290 (oldversioned, newversioned),
4291 (oldparent, newparent), (oldname, newname),
4292 (oldkind, newkind), (oldexe, newexe),
4293@@ -971,10 +981,18 @@
4294 _matching_to_tree_format = None
4295 _test_mutable_trees_to_test_trees = None
4296
4297+ def __init__(self, source, target):
4298+ super(InterGitTrees, self).__init__(source, target)
4299+ if self.source.store == self.target.store:
4300+ self.store = self.source.store
4301+ else:
4302+ self.store = OverlayObjectStore(
4303+ [self.source.store, self.target.store])
4304+ self.rename_detector = RenameDetector(self.store)
4305+
4306 @classmethod
4307 def is_compatible(cls, source, target):
4308- return (isinstance(source, GitRevisionTree) and
4309- isinstance(target, GitRevisionTree))
4310+ return isinstance(source, GitTree) and isinstance(target, GitTree)
4311
4312 def compare(self, want_unchanged=False, specific_files=None,
4313 extra_trees=None, require_versioned=False, include_root=False,
4314@@ -1012,7 +1030,25 @@
4315 def _iter_git_changes(self, want_unchanged=False, specific_files=None,
4316 require_versioned=False, extra_trees=None,
4317 want_unversioned=False, include_trees=True):
4318- raise NotImplementedError(self._iter_git_changes)
4319+ trees = [self.source]
4320+ if extra_trees is not None:
4321+ trees.extend(extra_trees)
4322+ if specific_files is not None:
4323+ specific_files = self.target.find_related_paths_across_trees(
4324+ specific_files, trees,
4325+ require_versioned=require_versioned)
4326+ # TODO(jelmer): Restrict to specific_files, for performance reasons.
4327+ with self.lock_read():
4328+ from_tree_sha, from_extras = self.source.git_snapshot(
4329+ want_unversioned=want_unversioned)
4330+ to_tree_sha, to_extras = self.target.git_snapshot(
4331+ want_unversioned=want_unversioned)
4332+ changes = tree_changes(
4333+ self.store, from_tree_sha, to_tree_sha,
4334+ include_trees=include_trees,
4335+ rename_detector=self.rename_detector,
4336+ want_unchanged=want_unchanged, change_type_same=True)
4337+ return changes, from_extras, to_extras
4338
4339 def find_target_path(self, path, recurse='none'):
4340 ret = self.find_target_paths([path], recurse=recurse)
4341@@ -1067,48 +1103,10 @@
4342 return ret
4343
4344
4345-class InterGitRevisionTrees(InterGitTrees):
4346- """InterTree that works between two git revision trees."""
4347-
4348- _matching_from_tree_format = None
4349- _matching_to_tree_format = None
4350- _test_mutable_trees_to_test_trees = None
4351-
4352- @classmethod
4353- def is_compatible(cls, source, target):
4354- return (isinstance(source, GitRevisionTree) and
4355- isinstance(target, GitRevisionTree))
4356-
4357- def _iter_git_changes(self, want_unchanged=False, specific_files=None,
4358- require_versioned=True, extra_trees=None,
4359- want_unversioned=False, include_trees=True):
4360- trees = [self.source]
4361- if extra_trees is not None:
4362- trees.extend(extra_trees)
4363- if specific_files is not None:
4364- specific_files = self.target.find_related_paths_across_trees(
4365- specific_files, trees,
4366- require_versioned=require_versioned)
4367-
4368- if (self.source._repository._git.object_store !=
4369- self.target._repository._git.object_store):
4370- store = OverlayObjectStore(
4371- [self.source._repository._git.object_store,
4372- self.target._repository._git.object_store])
4373- else:
4374- store = self.source._repository._git.object_store
4375- rename_detector = RenameDetector(store)
4376- changes = tree_changes(
4377- store, self.source.tree, self.target.tree,
4378- want_unchanged=want_unchanged, include_trees=include_trees,
4379- change_type_same=True, rename_detector=rename_detector)
4380- return changes, set(), set()
4381-
4382-
4383-_mod_tree.InterTree.register_optimiser(InterGitRevisionTrees)
4384-
4385-
4386-class MutableGitIndexTree(mutabletree.MutableTree):
4387+_mod_tree.InterTree.register_optimiser(InterGitTrees)
4388+
4389+
4390+class MutableGitIndexTree(mutabletree.MutableTree, GitTree):
4391
4392 def __init__(self):
4393 self._lock_mode = None
4394@@ -1117,6 +1115,9 @@
4395 self._index_dirty = False
4396 self._submodules = None
4397
4398+ def git_snapshot(self, want_unversioned=False):
4399+ return snapshot_workingtree(self, want_unversioned=want_unversioned)
4400+
4401 def is_versioned(self, path):
4402 with self.lock_read():
4403 path = encode_git_path(path.rstrip('/'))
4404@@ -1136,7 +1137,7 @@
4405 if self._lock_mode is None:
4406 raise errors.ObjectNotLocked(self)
4407 self._versioned_dirs = set()
4408- for p, i in self._recurse_index_entries():
4409+ for p, sha, mode in self.iter_git_objects():
4410 self._ensure_versioned_dir(posixpath.dirname(p))
4411
4412 def _ensure_versioned_dir(self, dirname):
4413@@ -1185,18 +1186,6 @@
4414 def _read_submodule_head(self, path):
4415 raise NotImplementedError(self._read_submodule_head)
4416
4417- def _submodule_info(self):
4418- if self._submodules is None:
4419- try:
4420- with self.get_file('.gitmodules') as f:
4421- config = GitConfigFile.from_file(f)
4422- self._submodules = {
4423- path: (url, section)
4424- for path, url, section in parse_submodules(config)}
4425- except errors.NoSuchFile:
4426- self._submodules = {}
4427- return self._submodules
4428-
4429 def _lookup_index(self, encoded_path):
4430 if not isinstance(encoded_path, bytes):
4431 raise TypeError(encoded_path)
4432@@ -1232,11 +1221,32 @@
4433 # TODO(jelmer): Keep track of dirty per index
4434 self._index_dirty = True
4435
4436- def _index_add_entry(self, path, kind, flags=0, reference_revision=None):
4437+ def _apply_index_changes(self, changes):
4438+ for (path, kind, executability, reference_revision,
4439+ symlink_target) in changes:
4440+ if kind is None or kind == 'directory':
4441+ (index, subpath) = self._lookup_index(
4442+ encode_git_path(path))
4443+ try:
4444+ self._index_del_entry(index, subpath)
4445+ except KeyError:
4446+ pass
4447+ else:
4448+ self._versioned_dirs = None
4449+ else:
4450+ self._index_add_entry(
4451+ path, kind,
4452+ reference_revision=reference_revision,
4453+ symlink_target=symlink_target)
4454+ self.flush()
4455+
4456+ def _index_add_entry(
4457+ self, path, kind, flags=0, reference_revision=None,
4458+ symlink_target=None):
4459 if kind == "directory":
4460 # Git indexes don't contain directories
4461 return
4462- if kind == "file":
4463+ elif kind == "file":
4464 blob = Blob()
4465 try:
4466 file, stat_val = self.get_file_with_stat(path)
4467@@ -1261,7 +1271,9 @@
4468 # old index
4469 stat_val = os.stat_result(
4470 (stat.S_IFLNK, 0, 0, 0, 0, 0, 0, 0, 0, 0))
4471- blob.set_raw_string(encode_git_path(self.get_symlink_target(path)))
4472+ if symlink_target is None:
4473+ symlink_target = self.get_symlink_target(path)
4474+ blob.set_raw_string(encode_git_path(symlink_target))
4475 # Add object to the repository if it didn't exist yet
4476 if blob.id not in self.store:
4477 self.store.add_object(blob)
4478@@ -1295,6 +1307,10 @@
4479 if self._versioned_dirs is not None:
4480 self._ensure_versioned_dir(index_path)
4481
4482+ def iter_git_objects(self):
4483+ for p, entry in self._recurse_index_entries():
4484+ yield p, entry.sha, entry.mode
4485+
4486 def _recurse_index_entries(self, index=None, basepath=b"",
4487 recurse_nested=False):
4488 # Iterate over all index entries
4489@@ -1381,18 +1397,8 @@
4490 elif kind == 'tree-reference':
4491 ie.reference_revision = self.get_reference_revision(path)
4492 else:
4493- try:
4494- data = self.get_file_text(path)
4495- except errors.NoSuchFile:
4496- data = None
4497- except IOError as e:
4498- if e.errno != errno.ENOENT:
4499- raise
4500- data = None
4501- if data is None:
4502- data = self.branch.repository._git.object_store[sha].data
4503- ie.text_sha1 = osutils.sha_string(data)
4504- ie.text_size = len(data)
4505+ ie.git_sha1 = sha
4506+ ie.text_size = size
4507 ie.executable = bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
4508 return ie
4509
4510@@ -1559,25 +1565,6 @@
4511 self._versioned_dirs = None
4512 self.flush()
4513
4514- def find_related_paths_across_trees(self, paths, trees=[],
4515- require_versioned=True):
4516- if paths is None:
4517- return None
4518-
4519- if require_versioned:
4520- trees = [self] + (trees if trees is not None else [])
4521- unversioned = set()
4522- for p in paths:
4523- for t in trees:
4524- if t.is_versioned(p):
4525- break
4526- else:
4527- unversioned.add(p)
4528- if unversioned:
4529- raise errors.PathsNotVersionedError(unversioned)
4530-
4531- return filter(self.is_versioned, paths)
4532-
4533 def path_content_summary(self, path):
4534 """See Tree.path_content_summary."""
4535 try:
4536@@ -1604,17 +1591,21 @@
4537 return (kind, None, None, None)
4538
4539 def stored_kind(self, relpath):
4540+ if relpath == '':
4541+ return 'directory'
4542 (index, index_path) = self._lookup_index(encode_git_path(relpath))
4543 if index is None:
4544- return kind
4545+ return None
4546 try:
4547 mode = index[index_path].mode
4548 except KeyError:
4549- return kind
4550+ for p in index:
4551+ if osutils.is_inside(
4552+ decode_git_path(index_path), decode_git_path(p)):
4553+ return 'directory'
4554+ return None
4555 else:
4556- if S_ISGITLINK(mode):
4557- return 'tree-reference'
4558- return 'directory'
4559+ return mode_kind(mode)
4560
4561 def kind(self, relpath):
4562 kind = osutils.file_kind(self.abspath(relpath))
4563@@ -1632,134 +1623,49 @@
4564 from .transform import GitTreeTransform
4565 return GitTreeTransform(self, pb=pb)
4566
4567- def preview_transform(self, pb=None):
4568- from .transform import GitTransformPreview
4569- return GitTransformPreview(self, pb=pb)
4570-
4571-
4572-class InterToIndexGitTree(InterGitTrees):
4573- """InterTree that works between a Git revision tree and an index."""
4574-
4575- def __init__(self, source, target):
4576- super(InterToIndexGitTree, self).__init__(source, target)
4577- if self.source.store == self.target.store:
4578- self.store = self.source.store
4579- else:
4580- self.store = OverlayObjectStore(
4581- [self.source.store, self.target.store])
4582- self.rename_detector = RenameDetector(self.store)
4583-
4584- @classmethod
4585- def is_compatible(cls, source, target):
4586- return (isinstance(source, GitRevisionTree) and
4587- isinstance(target, MutableGitIndexTree))
4588-
4589- def _iter_git_changes(self, want_unchanged=False, specific_files=None,
4590- require_versioned=False, extra_trees=None,
4591- want_unversioned=False, include_trees=True):
4592- trees = [self.source]
4593- if extra_trees is not None:
4594- trees.extend(extra_trees)
4595- if specific_files is not None:
4596- specific_files = self.target.find_related_paths_across_trees(
4597- specific_files, trees,
4598- require_versioned=require_versioned)
4599- # TODO(jelmer): Restrict to specific_files, for performance reasons.
4600- with self.lock_read():
4601- changes, target_extras = changes_between_git_tree_and_working_copy(
4602- self.source.store, self.source.tree,
4603- self.target, want_unchanged=want_unchanged,
4604- want_unversioned=want_unversioned,
4605- rename_detector=self.rename_detector,
4606- include_trees=include_trees)
4607- return changes, set(), target_extras
4608-
4609-
4610-_mod_tree.InterTree.register_optimiser(InterToIndexGitTree)
4611-
4612-
4613-class InterFromIndexGitTree(InterGitTrees):
4614- """InterTree that works between a Git revision tree and an index."""
4615-
4616- def __init__(self, source, target):
4617- super(InterFromIndexGitTree, self).__init__(source, target)
4618- if self.source.store == self.target.store:
4619- self.store = self.source.store
4620- else:
4621- self.store = OverlayObjectStore(
4622- [self.source.store, self.target.store])
4623- self.rename_detector = RenameDetector(self.store)
4624-
4625- @classmethod
4626- def is_compatible(cls, source, target):
4627- return (isinstance(target, GitRevisionTree) and
4628- isinstance(source, MutableGitIndexTree))
4629-
4630- def _iter_git_changes(self, want_unchanged=False, specific_files=None,
4631- require_versioned=False, extra_trees=None,
4632- want_unversioned=False, include_trees=True):
4633- trees = [self.source]
4634- if extra_trees is not None:
4635- trees.extend(extra_trees)
4636- if specific_files is not None:
4637- specific_files = self.target.find_related_paths_across_trees(
4638- specific_files, trees,
4639- require_versioned=require_versioned)
4640- # TODO(jelmer): Restrict to specific_files, for performance reasons.
4641- with self.lock_read():
4642- from_tree_sha, extras = snapshot_workingtree(self.source, want_unversioned=want_unversioned)
4643- return tree_changes(
4644- self.store, from_tree_sha, self.target.tree,
4645- include_trees=include_trees,
4646- rename_detector=self.rename_detector,
4647- want_unchanged=want_unchanged, change_type_same=True), extras
4648-
4649-
4650-_mod_tree.InterTree.register_optimiser(InterFromIndexGitTree)
4651-
4652-
4653-class InterIndexGitTree(InterGitTrees):
4654- """InterTree that works between a Git revision tree and an index."""
4655-
4656- def __init__(self, source, target):
4657- super(InterIndexGitTree, self).__init__(source, target)
4658- if self.source.store == self.target.store:
4659- self.store = self.source.store
4660- else:
4661- self.store = OverlayObjectStore(
4662- [self.source.store, self.target.store])
4663- self.rename_detector = RenameDetector(self.store)
4664-
4665- @classmethod
4666- def is_compatible(cls, source, target):
4667- return (isinstance(target, MutableGitIndexTree) and
4668- isinstance(source, MutableGitIndexTree))
4669-
4670- def _iter_git_changes(self, want_unchanged=False, specific_files=None,
4671- require_versioned=False, extra_trees=None,
4672- want_unversioned=False, include_trees=True):
4673- trees = [self.source]
4674- if extra_trees is not None:
4675- trees.extend(extra_trees)
4676- if specific_files is not None:
4677- specific_files = self.target.find_related_paths_across_trees(
4678- specific_files, trees,
4679- require_versioned=require_versioned)
4680- # TODO(jelmer): Restrict to specific_files, for performance reasons.
4681- with self.lock_read():
4682- from_tree_sha, from_extras = snapshot_workingtree(
4683- self.source, want_unversioned=want_unversioned)
4684- to_tree_sha, to_extras = snapshot_workingtree(
4685- self.target, want_unversioned=want_unversioned)
4686- changes = tree_changes(
4687- self.store, from_tree_sha, to_tree_sha,
4688- include_trees=include_trees,
4689- rename_detector=self.rename_detector,
4690- want_unchanged=want_unchanged, change_type_same=True)
4691- return changes, from_extras, to_extras
4692-
4693-
4694-_mod_tree.InterTree.register_optimiser(InterIndexGitTree)
4695+ def has_changes(self, _from_tree=None):
4696+ """Quickly check that the tree contains at least one commitable change.
4697+
4698+ :param _from_tree: tree to compare against to find changes (default to
4699+ the basis tree and is intended to be used by tests).
4700+
4701+ :return: True if a change is found. False otherwise
4702+ """
4703+ with self.lock_read():
4704+ # Check pending merges
4705+ if len(self.get_parent_ids()) > 1:
4706+ return True
4707+ if _from_tree is None:
4708+ _from_tree = self.basis_tree()
4709+ changes = self.iter_changes(_from_tree)
4710+ if self.supports_symlinks():
4711+ # Fast path for has_changes.
4712+ try:
4713+ change = next(changes)
4714+ if change.path[1] == '':
4715+ next(changes)
4716+ return True
4717+ except StopIteration:
4718+ # No changes
4719+ return False
4720+ else:
4721+ # Slow path for has_changes.
4722+ # Handle platforms that do not support symlinks in the
4723+ # conditional below. This is slower than the try/except
4724+ # approach below that but we don't have a choice as we
4725+ # need to be sure that all symlinks are removed from the
4726+ # entire changeset. This is because in platforms that
4727+ # do not support symlinks, they show up as None in the
4728+ # working copy as compared to the repository.
4729+ # Also, exclude root as mention in the above fast path.
4730+ changes = filter(
4731+ lambda c: c[6][0] != 'symlink' and c[4] != (None, None),
4732+ changes)
4733+ try:
4734+ next(iter(changes))
4735+ except StopIteration:
4736+ return False
4737+ return True
4738
4739
4740 def snapshot_workingtree(target, want_unversioned=False):
4741@@ -1809,21 +1715,21 @@
4742 target.store.add_object(blob)
4743 blobs[path] = (live_entry.sha, cleanup_mode(live_entry.mode))
4744 if want_unversioned:
4745- for e in target._iter_files_recursive(include_dirs=False):
4746+ for extra in target._iter_files_recursive(include_dirs=False):
4747 try:
4748- e, accessible = osutils.normalized_filename(e)
4749+ extra, accessible = osutils.normalized_filename(extra)
4750 except UnicodeDecodeError:
4751 raise errors.BadFilenameEncoding(
4752- e, osutils._fs_enc)
4753- np = encode_git_path(e)
4754+ extra, osutils._fs_enc)
4755+ np = encode_git_path(extra)
4756 if np in blobs:
4757 continue
4758- st = target._lstat(e)
4759+ st = target._lstat(extra)
4760 if stat.S_ISDIR(st.st_mode):
4761 blob = Tree()
4762 elif stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
4763 blob = blob_from_path_and_stat(
4764- target.abspath(e).encode(osutils._fs_enc), st)
4765+ target.abspath(extra).encode(osutils._fs_enc), st)
4766 else:
4767 continue
4768 target.store.add_object(blob)
4769@@ -1831,19 +1737,3 @@
4770 extras.add(np)
4771 return commit_tree(
4772 target.store, dirified + [(p, s, m) for (p, (s, m)) in blobs.items()]), extras
4773-
4774-
4775-def changes_between_git_tree_and_working_copy(source_store, from_tree_sha, target,
4776- want_unchanged=False,
4777- want_unversioned=False,
4778- rename_detector=None,
4779- include_trees=True):
4780- """Determine the changes between a git tree and a working tree with index.
4781-
4782- """
4783- to_tree_sha, extras = snapshot_workingtree(target, want_unversioned=want_unversioned)
4784- store = OverlayObjectStore([source_store, target.store])
4785- return tree_changes(
4786- store, from_tree_sha, to_tree_sha, include_trees=include_trees,
4787- rename_detector=rename_detector,
4788- want_unchanged=want_unchanged, change_type_same=True), extras
4789
4790=== modified file 'breezy/git/workingtree.py'
4791--- breezy/git/workingtree.py 2020-07-18 23:14:00 +0000
4792+++ breezy/git/workingtree.py 2020-08-23 01:05:31 +0000
4793@@ -44,6 +44,7 @@
4794 )
4795 import os
4796 import posixpath
4797+import re
4798 import stat
4799 import sys
4800
4801@@ -84,6 +85,101 @@
4802 )
4803
4804
4805+CONFLICT_SUFFIXES = ['.BASE', '.OTHER', '.THIS']
4806+
4807+
4808+# TODO: There should be a base revid attribute to better inform the user about
4809+# how the conflicts were generated.
4810+class TextConflict(_mod_conflicts.Conflict):
4811+ """The merge algorithm could not resolve all differences encountered."""
4812+
4813+ has_files = True
4814+
4815+ typestring = 'text conflict'
4816+
4817+ _conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
4818+
4819+ def associated_filenames(self):
4820+ return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
4821+
4822+ def _resolve(self, tt, winner_suffix):
4823+ """Resolve the conflict by copying one of .THIS or .OTHER into file.
4824+
4825+ :param tt: The TreeTransform where the conflict is resolved.
4826+ :param winner_suffix: Either 'THIS' or 'OTHER'
4827+
4828+ The resolution is symmetric, when taking THIS, item.THIS is renamed
4829+ into item and vice-versa. This takes one of the files as a whole
4830+ ignoring every difference that could have been merged cleanly.
4831+ """
4832+ # To avoid useless copies, we switch item and item.winner_suffix, only
4833+ # item will exist after the conflict has been resolved anyway.
4834+ item_tid = tt.trans_id_tree_path(self.path)
4835+ item_parent_tid = tt.get_tree_parent(item_tid)
4836+ winner_path = self.path + '.' + winner_suffix
4837+ winner_tid = tt.trans_id_tree_path(winner_path)
4838+ winner_parent_tid = tt.get_tree_parent(winner_tid)
4839+ # Switch the paths to preserve the content
4840+ tt.adjust_path(osutils.basename(self.path),
4841+ winner_parent_tid, winner_tid)
4842+ tt.adjust_path(osutils.basename(winner_path),
4843+ item_parent_tid, item_tid)
4844+ tt.unversion_file(item_tid)
4845+ tt.version_file(winner_tid)
4846+ tt.apply()
4847+
4848+ def action_auto(self, tree):
4849+ # GZ 2012-07-27: Using NotImplementedError to signal that a conflict
4850+ # can't be auto resolved does not seem ideal.
4851+ try:
4852+ kind = tree.kind(self.path)
4853+ except errors.NoSuchFile:
4854+ return
4855+ if kind != 'file':
4856+ raise NotImplementedError("Conflict is not a file")
4857+ conflict_markers_in_line = self._conflict_re.search
4858+ with tree.get_file(self.path) as f:
4859+ for line in f:
4860+ if conflict_markers_in_line(line):
4861+ raise NotImplementedError("Conflict markers present")
4862+
4863+ def _resolve_with_cleanups(self, tree, *args, **kwargs):
4864+ with tree.transform() as tt:
4865+ self._resolve(tt, *args, **kwargs)
4866+
4867+ def action_take_this(self, tree):
4868+ self._resolve_with_cleanups(tree, 'THIS')
4869+
4870+ def action_take_other(self, tree):
4871+ self._resolve_with_cleanups(tree, 'OTHER')
4872+
4873+ def do(self, action, tree):
4874+ """Apply the specified action to the conflict.
4875+
4876+ :param action: The method name to call.
4877+
4878+ :param tree: The tree passed as a parameter to the method.
4879+ """
4880+ meth = getattr(self, 'action_%s' % action, None)
4881+ if meth is None:
4882+ raise NotImplementedError(self.__class__.__name__ + '.' + action)
4883+ meth(tree)
4884+
4885+ def action_done(self, tree):
4886+ """Mark the conflict as solved once it has been handled."""
4887+ # This method does nothing but simplifies the design of upper levels.
4888+ pass
4889+
4890+ def describe(self):
4891+ return 'Text conflict in %(path)s' % self.__dict__
4892+
4893+ def __str__(self):
4894+ return self.describe()
4895+
4896+ def __repr__(self):
4897+ return "%s(%r)" % (type(self).__name__, self.path)
4898+
4899+
4900 class GitWorkingTree(MutableGitIndexTree, workingtree.WorkingTree):
4901 """A Git working tree."""
4902
4903@@ -330,8 +426,8 @@
4904 def recurse_directory_to_add_files(directory):
4905 # Recurse directory and add all files
4906 # so we can check if they have changed.
4907- for parent_info, file_infos in self.walkdirs(directory):
4908- for relpath, basename, kind, lstat, fileid, kind in file_infos:
4909+ for parent_path, file_infos in self.walkdirs(directory):
4910+ for relpath, basename, kind, lstat, kind in file_infos:
4911 # Is it versioned or ignored?
4912 if self.is_versioned(relpath):
4913 # Add nested content for deletion.
4914@@ -569,7 +665,7 @@
4915 """
4916 with self.lock_read():
4917 index_paths = set(
4918- [decode_git_path(p) for p, i in self._recurse_index_entries()])
4919+ [decode_git_path(p) for p, sha, mode in self.iter_git_objects()])
4920 all_paths = set(self._iter_files_recursive(include_dirs=False))
4921 return iter(all_paths - index_paths)
4922
4923@@ -867,8 +963,7 @@
4924 conflicts = _mod_conflicts.ConflictList()
4925 for item_path, value in self.index.iteritems():
4926 if value.flags & FLAG_STAGEMASK:
4927- conflicts.append(_mod_conflicts.TextConflict(
4928- decode_git_path(item_path)))
4929+ conflicts.append(TextConflict(decode_git_path(item_path)))
4930 return conflicts
4931
4932 def set_conflicts(self, conflicts):
4933@@ -883,7 +978,6 @@
4934 self._set_conflicted(path, path in by_path)
4935
4936 def _set_conflicted(self, path, conflicted):
4937- trace.mutter('change conflict: %r -> %r', path, conflicted)
4938 value = self.index[path]
4939 self._index_dirty = True
4940 if conflicted:
4941@@ -909,8 +1003,8 @@
4942 """Walk the directories of this tree.
4943
4944 returns a generator which yields items in the form:
4945- ((curren_directory_path, fileid),
4946- [(file1_path, file1_name, file1_kind, (lstat), file1_id,
4947+ (current_directory_path,
4948+ [(file1_path, file1_name, file1_kind, (lstat),
4949 file1_kind), ... ])
4950
4951 This API returns a generator, which is only valid during the current
4952@@ -974,20 +1068,20 @@
4953 - (current_inv[0][0] < cur_disk_dir_relpath))
4954 if direction > 0:
4955 # disk is before inventory - unknown
4956- dirblock = [(relpath, basename, kind, stat, None, None) for
4957+ dirblock = [(relpath, basename, kind, stat, None) for
4958 relpath, basename, kind, stat, top_path in
4959 cur_disk_dir_content]
4960- yield (cur_disk_dir_relpath, None), dirblock
4961+ yield cur_disk_dir_relpath, dirblock
4962 try:
4963 current_disk = next(disk_iterator)
4964 except StopIteration:
4965 disk_finished = True
4966 elif direction < 0:
4967 # inventory is before disk - missing.
4968- dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
4969+ dirblock = [(relpath, basename, 'unknown', None, kind)
4970 for relpath, basename, dkind, stat, fileid, kind in
4971 current_inv[1]]
4972- yield (current_inv[0][0], current_inv[0][1]), dirblock
4973+ yield current_inv[0][0], dirblock
4974 try:
4975 current_inv = next(inventory_iterator)
4976 except StopIteration:
4977@@ -1005,23 +1099,22 @@
4978 # versioned, present file
4979 dirblock.append((inv_row[0],
4980 inv_row[1], disk_row[2],
4981- disk_row[3], inv_row[4],
4982- inv_row[5]))
4983+ disk_row[3], inv_row[5]))
4984 elif len(path_elements[0]) == 5:
4985 # unknown disk file
4986 dirblock.append(
4987 (path_elements[0][0], path_elements[0][1],
4988 path_elements[0][2], path_elements[0][3],
4989- None, None))
4990+ None))
4991 elif len(path_elements[0]) == 6:
4992 # versioned, absent file.
4993 dirblock.append(
4994 (path_elements[0][0], path_elements[0][1],
4995- 'unknown', None, path_elements[0][4],
4996+ 'unknown', None,
4997 path_elements[0][5]))
4998 else:
4999 raise NotImplementedError('unreachable code')
5000- yield current_inv[0], dirblock
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches