Merge lp:~jelmer/brz-git/iter-changes-missing into lp:brz-git
- iter-changes-missing
- Merge into trunk
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 |
Related bugs: |
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) |