Merge lp:~jelmer/brz-git/iter-changes-missing into lp:brz-git

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 1902
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz-git/iter-changes-missing
Merge into: lp:brz-git
Diff against target: 481 lines (+220/-104)
5 files modified
filegraph.py (+2/-2)
tests/test_workingtree.py (+122/-0)
tree.py (+46/-26)
workingtree.py (+50/-75)
xfail (+0/-1)
To merge this branch: bzr merge lp:~jelmer/brz-git/iter-changes-missing
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+342067@code.launchpad.net

Commit message

Allow missing items in iter_changes.

Description of the change

Allow missing items in iter_changes.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'filegraph.py'
2--- filegraph.py 2018-03-17 19:20:55 +0000
3+++ filegraph.py 2018-03-26 00:33:37 +0000
4@@ -44,7 +44,7 @@
5 target_mode, target_sha = tree_lookup_path(self.store.__getitem__,
6 commit.tree, path)
7 if path == '':
8- target_mode = stat.S_IFDIR | 0644
9+ target_mode = stat.S_IFDIR
10 if target_mode is None:
11 raise AssertionError("sha %r for %r in %r" % (target_sha, path, commit_id))
12 while True:
13@@ -58,7 +58,7 @@
14 else:
15 parent_commits.append(parent_commit)
16 if path == '':
17- mode = stat.S_IFDIR | 0644
18+ mode = stat.S_IFDIR
19 # Candidate found iff, mode or text changed,
20 # or is a directory that didn't previously exist.
21 if mode != target_mode or (
22
23=== modified file 'tests/test_workingtree.py'
24--- tests/test_workingtree.py 2018-03-22 17:40:16 +0000
25+++ tests/test_workingtree.py 2018-03-26 00:33:37 +0000
26@@ -19,9 +19,19 @@
27
28 from __future__ import absolute_import
29
30+import os
31+import stat
32+
33+from dulwich.objects import (
34+ Blob,
35+ Tree,
36+ ZERO_SHA,
37+ )
38+
39 from .... import conflicts as _mod_conflicts
40 from ..workingtree import (
41 FLAG_STAGEMASK,
42+ changes_between_git_tree_and_working_copy,
43 )
44 from ....tests import TestCaseWithTransport
45
46@@ -51,3 +61,115 @@
47 self.assertTrue(self.tree.is_versioned('a'))
48 self.tree.revert(['a'])
49 self.assertFalse(self.tree.is_versioned('a'))
50+
51+
52+class ChangesBetweenGitTreeAndWorkingCopyTests(TestCaseWithTransport):
53+
54+ def setUp(self):
55+ super(ChangesBetweenGitTreeAndWorkingCopyTests, self).setUp()
56+ self.wt = self.make_branch_and_tree('.', format='git')
57+
58+ def expectDelta(self, expected_changes,
59+ expected_extras=None, want_unversioned=False):
60+ store = self.wt.branch.repository._git.object_store
61+ try:
62+ tree_id = store[self.wt.branch.repository._git.head()].tree
63+ except KeyError:
64+ tree_id = None
65+ changes, extras = changes_between_git_tree_and_working_copy(
66+ store, tree_id, self.wt, want_unversioned=want_unversioned)
67+ self.assertEqual(expected_changes, list(changes))
68+ if expected_extras is None:
69+ expected_extras = set()
70+ self.assertEqual(set(expected_extras), set(extras))
71+
72+ def test_empty(self):
73+ self.expectDelta(
74+ [((None, ''), (None, stat.S_IFDIR), (None, Tree().id))])
75+
76+ def test_added_file(self):
77+ self.build_tree(['a'])
78+ self.wt.add(['a'])
79+ a = Blob.from_string('contents of a\n')
80+ t = Tree()
81+ t.add("a", stat.S_IFREG | 0o644, a.id)
82+ self.expectDelta(
83+ [((None, ''), (None, stat.S_IFDIR), (None, t.id)),
84+ ((None, 'a'), (None, stat.S_IFREG | 0o644), (None, a.id))])
85+
86+ def test_added_unknown_file(self):
87+ self.build_tree(['a'])
88+ t = Tree()
89+ self.expectDelta(
90+ [((None, ''), (None, stat.S_IFDIR), (None, t.id))])
91+ a = Blob.from_string('contents of a\n')
92+ t = Tree()
93+ t.add("a", stat.S_IFREG | 0o644, a.id)
94+ self.expectDelta(
95+ [((None, ''), (None, stat.S_IFDIR), (None, t.id)),
96+ ((None, 'a'), (None, stat.S_IFREG | 0o644), (None, a.id))],
97+ ['a'],
98+ want_unversioned=True)
99+
100+ def test_missing_added_file(self):
101+ self.build_tree(['a'])
102+ self.wt.add(['a'])
103+ os.unlink('a')
104+ a = Blob.from_string('contents of a\n')
105+ t = Tree()
106+ t.add("a", 0, ZERO_SHA)
107+ self.expectDelta(
108+ [((None, ''), (None, stat.S_IFDIR), (None, t.id)),
109+ ((None, 'a'), (None, 0), (None, ZERO_SHA))],
110+ [])
111+
112+ def test_missing_versioned_file(self):
113+ self.build_tree(['a'])
114+ self.wt.add(['a'])
115+ self.wt.commit('')
116+ os.unlink('a')
117+ a = Blob.from_string('contents of a\n')
118+ oldt = Tree()
119+ oldt.add("a", stat.S_IFREG | 0o644, a.id)
120+ newt = Tree()
121+ newt.add("a", 0, ZERO_SHA)
122+ self.expectDelta(
123+ [(('', ''), (stat.S_IFDIR, stat.S_IFDIR), (oldt.id, newt.id)),
124+ (('a', 'a'), (stat.S_IFREG|0o644, 0), (a.id, ZERO_SHA))])
125+
126+ def test_versioned_replace_by_dir(self):
127+ self.build_tree(['a'])
128+ self.wt.add(['a'])
129+ self.wt.commit('')
130+ os.unlink('a')
131+ os.mkdir('a')
132+ olda = Blob.from_string('contents of a\n')
133+ oldt = Tree()
134+ oldt.add("a", stat.S_IFREG | 0o644, olda.id)
135+ newt = Tree()
136+ newa = Tree()
137+ newt.add("a", stat.S_IFDIR, newa.id)
138+ self.expectDelta([
139+ (('', ''),
140+ (stat.S_IFDIR, stat.S_IFDIR),
141+ (oldt.id, newt.id)),
142+ (('a', 'a'), (stat.S_IFREG | 0o644, stat.S_IFDIR), (olda.id, newa.id))
143+ ], want_unversioned=False)
144+ self.expectDelta([
145+ (('', ''),
146+ (stat.S_IFDIR, stat.S_IFDIR),
147+ (oldt.id, newt.id)),
148+ (('a', 'a'), (stat.S_IFREG | 0o644, stat.S_IFDIR), (olda.id, newa.id))
149+ ], want_unversioned=True)
150+
151+ def test_extra(self):
152+ self.build_tree(['a'])
153+ newa = Blob.from_string('contents of a\n')
154+ newt = Tree()
155+ newt.add("a", stat.S_IFREG | 0o644, newa.id)
156+ self.expectDelta([
157+ ((None, ''),
158+ (None, stat.S_IFDIR),
159+ (None, newt.id)),
160+ ((None, 'a'), (None, stat.S_IFREG | 0o644), (None, newa.id))
161+ ], ['a'], want_unversioned=True)
162
163=== modified file 'tree.py'
164--- tree.py 2018-03-25 13:27:11 +0000
165+++ tree.py 2018-03-26 00:33:37 +0000
166@@ -559,12 +559,15 @@
167 return ret
168
169
170-def changes_from_git_changes(changes, mapping, specific_files=None, include_unchanged=False):
171+def changes_from_git_changes(changes, mapping, specific_files=None, include_unchanged=False,
172+ target_extras=None):
173 """Create a iter_changes-like generator from a git stream.
174
175 source and target are iterators over tuples with:
176 (filename, sha, mode)
177 """
178+ if target_extras is None:
179+ target_extras = set()
180 for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
181 if not (specific_files is None or
182 (oldpath is not None and osutils.is_inside_or_parent_of_any(specific_files, oldpath)) or
183@@ -581,12 +584,16 @@
184 oldkind = None
185 oldname = None
186 oldparent = None
187+ oldversioned = False
188 else:
189+ oldversioned = True
190 oldpath = oldpath.decode("utf-8")
191- if oldmode is None:
192- raise ValueError
193- oldexe = mode_is_executable(oldmode)
194- oldkind = mode_kind(oldmode)
195+ if oldmode:
196+ oldexe = mode_is_executable(oldmode)
197+ oldkind = mode_kind(oldmode)
198+ else:
199+ oldexe = False
200+ oldkind = None
201 if oldpath == u'':
202 oldparent = None
203 oldname = ''
204@@ -599,14 +606,16 @@
205 newkind = None
206 newname = None
207 newparent = None
208+ newversioned = False
209 else:
210- newpath = newpath.decode("utf-8")
211- if newmode is not None:
212+ newversioned = (newpath not in target_extras)
213+ if newmode:
214 newexe = mode_is_executable(newmode)
215 newkind = mode_kind(newmode)
216 else:
217 newexe = False
218 newkind = None
219+ newpath = newpath.decode("utf-8")
220 if newpath == u'':
221 newparent = None
222 newname = u''
223@@ -618,7 +627,7 @@
224 oldpath == newpath):
225 continue
226 yield (fileid, (oldpath, newpath), (oldsha != newsha),
227- (oldpath is not None, newpath is not None),
228+ (oldversioned, newversioned),
229 (oldparent, newparent), (oldname, newname),
230 (oldkind, newkind), (oldexe, newexe))
231
232@@ -638,28 +647,38 @@
233 def compare(self, want_unchanged=False, specific_files=None,
234 extra_trees=None, require_versioned=False, include_root=False,
235 want_unversioned=False):
236- changes = self._iter_git_changes(want_unchanged=want_unchanged,
237- require_versioned=require_versioned,
238- specific_files=specific_files,
239- extra_trees=extra_trees)
240- source_fileid_map = self.source._fileid_map
241- target_fileid_map = self.target._fileid_map
242- return tree_delta_from_git_changes(changes, self.target.mapping,
243- (source_fileid_map, target_fileid_map),
244- specific_files=specific_files, include_root=include_root)
245+ with self.lock_read():
246+ changes, target_extras = self._iter_git_changes(
247+ want_unchanged=want_unchanged,
248+ require_versioned=require_versioned,
249+ specific_files=specific_files,
250+ extra_trees=extra_trees,
251+ want_unversioned=want_unversioned)
252+ source_fileid_map = self.source._fileid_map
253+ target_fileid_map = self.target._fileid_map
254+ return tree_delta_from_git_changes(changes, self.target.mapping,
255+ (source_fileid_map, target_fileid_map),
256+ specific_files=specific_files, include_root=include_root)
257
258 def iter_changes(self, include_unchanged=False, specific_files=None,
259 pb=None, extra_trees=[], require_versioned=True,
260 want_unversioned=False):
261- changes = self._iter_git_changes(want_unchanged=include_unchanged,
262- require_versioned=require_versioned,
263- specific_files=specific_files,
264- extra_trees=extra_trees)
265- return changes_from_git_changes(changes, self.target.mapping,
266- specific_files=specific_files, include_unchanged=include_unchanged)
267+ with self.lock_read():
268+ changes, target_extras = self._iter_git_changes(
269+ want_unchanged=include_unchanged,
270+ require_versioned=require_versioned,
271+ specific_files=specific_files,
272+ extra_trees=extra_trees,
273+ want_unversioned=want_unversioned)
274+ return changes_from_git_changes(
275+ changes, self.target.mapping,
276+ specific_files=specific_files,
277+ include_unchanged=include_unchanged,
278+ target_extras=target_extras)
279
280 def _iter_git_changes(self, want_unchanged=False, specific_files=None,
281- require_versioned=False, extra_trees=None):
282+ require_versioned=False, extra_trees=None,
283+ want_unversioned=False):
284 raise NotImplementedError(self._iter_git_changes)
285
286
287@@ -676,7 +695,8 @@
288 isinstance(target, GitRevisionTree))
289
290 def _iter_git_changes(self, want_unchanged=False, specific_files=None,
291- require_versioned=True, extra_trees=None):
292+ require_versioned=True, extra_trees=None,
293+ want_unversioned=False):
294 trees = [self.source]
295 if extra_trees is not None:
296 trees.extend(extra_trees)
297@@ -692,7 +712,7 @@
298 store = self.source._repository._git.object_store
299 return self.source._repository._git.object_store.tree_changes(
300 self.source.tree, self.target.tree, want_unchanged=want_unchanged,
301- include_trees=True, change_type_same=True)
302+ include_trees=True, change_type_same=True), set()
303
304
305 _mod_tree.InterTree.register_optimiser(InterGitRevisionTrees)
306
307=== modified file 'workingtree.py'
308--- workingtree.py 2018-03-24 14:16:00 +0000
309+++ workingtree.py 2018-03-26 00:33:37 +0000
310@@ -35,8 +35,9 @@
311 changes_from_tree,
312 cleanup_mode,
313 commit_tree,
314+ index_entry_from_path,
315 index_entry_from_stat,
316- iter_fresh_blobs,
317+ iter_fresh_entries,
318 blob_from_path_and_stat,
319 FLAG_STAGEMASK,
320 validate_path,
321@@ -49,6 +50,7 @@
322 Tree,
323 S_IFGITLINK,
324 S_ISGITLINK,
325+ ZERO_SHA,
326 )
327 from dulwich.repo import Repo
328 import os
329@@ -1185,7 +1187,8 @@
330 isinstance(target, GitWorkingTree))
331
332 def _iter_git_changes(self, want_unchanged=False, specific_files=None,
333- require_versioned=False, include_root=False, extra_trees=None):
334+ require_versioned=False, extra_trees=None,
335+ want_unversioned=False):
336 trees = [self.source]
337 if extra_trees is not None:
338 trees.extend(extra_trees)
339@@ -1198,85 +1201,57 @@
340 return changes_between_git_tree_and_working_copy(
341 self.source.store, self.source.tree,
342 self.target, want_unchanged=want_unchanged,
343- include_root=include_root)
344-
345- def compare(self, want_unchanged=False, specific_files=None,
346- extra_trees=None, require_versioned=False, include_root=False,
347- want_unversioned=False):
348- with self.lock_read():
349- changes = self._iter_git_changes(
350- want_unchanged=want_unchanged,
351- specific_files=specific_files,
352- require_versioned=require_versioned,
353- include_root=include_root,
354- extra_trees=extra_trees)
355- source_fileid_map = self.source._fileid_map
356- target_fileid_map = self.target._fileid_map
357- ret = tree_delta_from_git_changes(changes, self.target.mapping,
358- (source_fileid_map, target_fileid_map),
359- specific_files=specific_files, require_versioned=require_versioned,
360- include_root=include_root)
361- if want_unversioned:
362- for e in self.target.extras():
363- ret.unversioned.append(
364- (osutils.normalized_filename(e)[0], None,
365- osutils.file_kind(self.target.abspath(e))))
366- return ret
367-
368- def iter_changes(self, include_unchanged=False, specific_files=None,
369- pb=None, extra_trees=[], require_versioned=True,
370- want_unversioned=False):
371- with self.lock_read():
372- changes = self._iter_git_changes(
373- want_unchanged=include_unchanged,
374- specific_files=specific_files,
375- require_versioned=require_versioned,
376- extra_trees=extra_trees)
377- if want_unversioned:
378- changes = itertools.chain(
379- changes,
380- untracked_changes(self.target))
381- return changes_from_git_changes(
382- changes, self.target.mapping,
383- specific_files=specific_files,
384- include_unchanged=include_unchanged)
385+ want_unversioned=want_unversioned)
386
387
388 tree.InterTree.register_optimiser(InterIndexGitTree)
389
390
391-def untracked_changes(tree):
392- for e in tree.extras():
393- ap = tree.abspath(e)
394- st = os.lstat(ap)
395- try:
396- np, accessible = osutils.normalized_filename(e)
397- except UnicodeDecodeError:
398- raise errors.BadFilenameEncoding(
399- e, osutils._fs_enc)
400- if stat.S_ISDIR(st.st_mode):
401- obj_id = Tree().id
402- else:
403- obj_id = blob_from_path_and_stat(ap.encode('utf-8'), st).id
404- yield ((None, np), (None, st.st_mode), (None, obj_id))
405-
406-
407-def changes_between_git_tree_and_index(store, from_tree_sha, target,
408- want_unchanged=False, update_index=False):
409- """Determine the changes between a git tree and a working tree with index.
410-
411- """
412- to_tree_sha = target.index.commit(store)
413- return store.tree_changes(from_tree_sha, to_tree_sha, include_trees=True,
414- want_unchanged=want_unchanged, change_type_same=True)
415-
416-
417 def changes_between_git_tree_and_working_copy(store, from_tree_sha, target,
418- want_unchanged=False, update_index=False, include_root=False):
419+ want_unchanged=False, want_unversioned=False):
420 """Determine the changes between a git tree and a working tree with index.
421
422 """
423- blobs = iter_fresh_blobs(target.index, target.abspath('.').encode(sys.getfilesystemencoding()))
424- to_tree_sha = commit_tree(store, blobs)
425- return store.tree_changes(from_tree_sha, to_tree_sha, include_trees=True,
426- want_unchanged=want_unchanged, change_type_same=True)
427+ extras = set()
428+ blobs = {}
429+ # Report dirified directories to commit_tree first, so that they can be
430+ # replaced with non-empty directories if they have contents.
431+ dirified = []
432+ target_root_path = target.abspath('.').encode(sys.getfilesystemencoding())
433+ for path, index_entry in target.index.iteritems():
434+ try:
435+ live_entry = index_entry_from_path(
436+ target.abspath(path.decode('utf-8')).encode(osutils._fs_enc))
437+ except EnvironmentError as e:
438+ if e.errno == errno.ENOENT:
439+ # Entry was removed; keep it listed, but mark it as gone.
440+ blobs[path] = (ZERO_SHA, 0)
441+ elif e.errno == errno.EISDIR:
442+ # Entry was turned into a directory
443+ dirified.append((path, Tree().id, stat.S_IFDIR))
444+ store.add_object(Tree())
445+ else:
446+ raise
447+ else:
448+ blobs[path] = (live_entry.sha, cleanup_mode(live_entry.mode))
449+ if want_unversioned:
450+ for e in target.extras():
451+ ap = target.abspath(e)
452+ st = os.lstat(ap)
453+ try:
454+ np, accessible = osutils.normalized_filename(e)
455+ except UnicodeDecodeError:
456+ raise errors.BadFilenameEncoding(
457+ e, osutils._fs_enc)
458+ if stat.S_ISDIR(st.st_mode):
459+ blob = Tree()
460+ else:
461+ blob = blob_from_path_and_stat(ap.encode('utf-8'), st)
462+ store.add_object(blob)
463+ np = np.encode('utf-8')
464+ blobs[np] = (blob.id, st.st_mode)
465+ extras.add(np)
466+ to_tree_sha = commit_tree(store, dirified + [(p, s, m) for (p, (s, m)) in blobs.iteritems()])
467+ return store.tree_changes(
468+ from_tree_sha, to_tree_sha, include_trees=True,
469+ want_unchanged=want_unchanged, change_type_same=True), extras
470
471=== modified file 'xfail'
472--- xfail 2018-03-25 14:14:01 +0000
473+++ xfail 2018-03-26 00:33:37 +0000
474@@ -40,7 +40,6 @@
475 breezy.tests.per_workingtree.test_annotate_iter.TestAnnotateIter.test_annotate_same_as_merge_parent(GitWorkingTreeFormat)
476 breezy.tests.per_workingtree.test_annotate_iter.TestAnnotateIter.test_annotate_same_as_merge_parent_supersedes(GitWorkingTreeFormat)
477 breezy.tests.per_workingtree.test_annotate_iter.TestAnnotateIter.test_annotate_same_as_parent(GitWorkingTreeFormat)
478-breezy.tests.per_workingtree.test_commit.TestCommit.test_commit_aborted_does_not_apply_automatic_changes_bug_282402(GitWorkingTreeFormat)
479 breezy.tests.per_workingtree.test_merge_from_branch.TestMergedBranch.test_file1_deleted_in_dir(GitWorkingTreeFormat)
480 breezy.tests.per_workingtree.test_merge_from_branch.TestMergedBranch.test_file3_deleted_in_root(GitWorkingTreeFormat)
481 breezy.tests.per_workingtree.test_merge_from_branch.TestMergedBranch.test_file3_in_root_conflicted(GitWorkingTreeFormat)

Subscribers

People subscribed via source and target branches

to all changes: