Skip to content

Commit 22743d3

Browse files
committed
fix: mark copy-only diff target as Added in DiffView
- parse copy metadata so copy target gets Added state (A icon) - update state tracking to apply to displayed target path only - avoid listing copy source as a separate file entry - add/strengthen tests for copy-only and incremental copy metadata parsing
1 parent 75ac949 commit 22743d3

File tree

3 files changed

+68
-40
lines changed

3 files changed

+68
-40
lines changed

qgitc/difffetcher.py

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,20 @@ def parse(self, data: bytes):
3939
lines = data.split(self.separator)
4040
fullFileAStr = None
4141
fullFileBStr = None
42+
fullDisplayFileStr = None
4243
fileState = FileState.Normal
4344

4445
def _updateFileState():
45-
nonlocal fullFileAStr, fullFileBStr, fileState
46+
nonlocal fullFileAStr, fullFileBStr, fullDisplayFileStr, fileState
4647
if fileState != FileState.Normal:
47-
if fullFileAStr:
48-
self._fileStates[fullFileAStr] = fileState
49-
if fullFileAStr in fileItems:
50-
fileItems[fullFileAStr].state = fileState
51-
if fullFileBStr:
52-
self._fileStates[fullFileBStr] = fileState
53-
if fullFileBStr in fileItems:
54-
fileItems[fullFileBStr].state = fileState
48+
fileToUpdate = fullDisplayFileStr or fullFileBStr or fullFileAStr
49+
if fileToUpdate:
50+
self._fileStates[fileToUpdate] = fileState
51+
if fileToUpdate in fileItems:
52+
fileItems[fileToUpdate].state = fileState
5553
fullFileAStr = None
5654
fullFileBStr = None
55+
fullDisplayFileStr = None
5756
fileState = FileState.Normal
5857

5958
for line in lines:
@@ -75,13 +74,6 @@ def _updateFileState():
7574

7675
fullFileA = self.makeFilePath(fileA)
7776
fullFileAStr = fullFileA.decode(diff_encoding)
78-
fileItems[fullFileAStr] = FileInfo(self._row)
79-
# Apply previously tracked state from earlier parse calls
80-
if fullFileAStr in self._fileStates:
81-
fileItems[fullFileAStr].state = self._fileStates[fullFileAStr]
82-
83-
# Store current file for incremental parsing
84-
self._currentFileA = fullFileAStr
8577

8678
# renames, keep new file name only
8779
if fileB and fileB != fileA:
@@ -91,9 +83,17 @@ def _updateFileState():
9183
fileItems[fullFileBStr] = FileInfo(self._row)
9284
if fullFileBStr in self._fileStates:
9385
fileItems[fullFileBStr].state = self._fileStates[fullFileBStr]
86+
fullDisplayFileStr = fullFileBStr
87+
self._currentFileA = fullFileAStr
9488
self._currentFileB = fullFileBStr
9589
else:
9690
lineItems.append((DiffType.File, fullFileA))
91+
fileItems[fullFileAStr] = FileInfo(self._row)
92+
# Apply previously tracked state from earlier parse calls
93+
if fullFileAStr in self._fileStates:
94+
fileItems[fullFileAStr].state = self._fileStates[fullFileAStr]
95+
fullDisplayFileStr = fullFileAStr
96+
self._currentFileA = fullFileAStr
9797
self._currentFileB = None
9898

9999
self._row += 1
@@ -146,35 +146,27 @@ def _updateFileState():
146146
fileState = FileState.Renamed
147147
elif fileState == FileState.Modified:
148148
fileState = FileState.RenamedModified
149+
elif line.startswith(b"copy to "):
150+
fileState = FileState.Added
149151
elif line.startswith(b"index "):
150152
if fileState == FileState.Renamed:
151153
fileState = FileState.RenamedModified
152154
elif fileState == FileState.Normal:
153155
fileState = FileState.Modified
154156

155-
fileAToUpdate = fullFileAStr or self._currentFileA
156-
fileBToUpdate = fullFileBStr or self._currentFileB
157-
158-
if fileAToUpdate:
159-
oldState = self._fileStates.get(fileAToUpdate)
160-
self._fileStates[fileAToUpdate] = fileState
161-
# If file not in fileItems yet (metadata from previous chunk)
162-
if fileAToUpdate not in fileItems:
163-
if oldState != fileState:
164-
self.fileStateChanged.emit(
165-
fileAToUpdate, fileState)
166-
else:
167-
fileItems[fileAToUpdate].state = fileState
168-
169-
if fileBToUpdate:
170-
oldState = self._fileStates.get(fileBToUpdate)
171-
self._fileStates[fileBToUpdate] = fileState
172-
if fileBToUpdate not in fileItems:
173-
if oldState != fileState:
174-
self.fileStateChanged.emit(
175-
fileBToUpdate, fileState)
176-
else:
177-
fileItems[fileBToUpdate].state = fileState
157+
if fileState != FileState.Normal:
158+
fileToUpdate = fullDisplayFileStr or fullFileBStr or \
159+
fullFileAStr or self._currentFileB or self._currentFileA
160+
if fileToUpdate:
161+
oldState = self._fileStates.get(fileToUpdate)
162+
self._fileStates[fileToUpdate] = fileState
163+
# If file not in fileItems yet (metadata from previous chunk)
164+
if fileToUpdate not in fileItems:
165+
if oldState != fileState:
166+
self.fileStateChanged.emit(
167+
fileToUpdate, fileState)
168+
else:
169+
fileItems[fileToUpdate].state = fileState
178170

179171
if itemType != DiffType.Diff:
180172
line = line.rstrip(b'\r')

tests/test_difffetcher.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,9 +736,12 @@ def test_copy_file(self):
736736
b'\x00'
737737
])
738738

739-
lineItems, fileItems = self._parse_and_get_results(diff_data)
739+
_, fileItems = self._parse_and_get_results(diff_data)
740740

741+
self.assertNotIn('original.txt', fileItems)
741742
self.assertIn('copy.txt', fileItems)
743+
self.assertEqual(fileItems['copy.txt'].state, FileState.Added,
744+
"Copy target should be Added")
742745

743746
def test_no_newline_at_end_of_file(self):
744747
"""

tests/test_difffetcher_bugs.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,36 @@ def test_true_incremental_parsing_across_calls(self):
485485
# foo.txt should NOT be in fileItems2 since it's a state-only update
486486
self.assertNotIn('foo.txt', fileItems2,
487487
"foo.txt should not be in fileItems2 (emitted via fileStateChanged instead)")
488+
489+
def test_true_incremental_copy_metadata_updates_target_state(self):
490+
"""copy metadata in a later chunk should emit Added for copy target."""
491+
chunk1 = b'\x00'.join([
492+
b'diff --git a/guides/README.md b/.github/copilot-instructions.md',
493+
b'\x00'
494+
])
495+
496+
_, fileItems1 = self._parse(chunk1)
497+
self.assertIn('.github/copilot-instructions.md', fileItems1)
498+
self.assertEqual(
499+
fileItems1['.github/copilot-instructions.md'].state,
500+
FileState.Normal
501+
)
502+
503+
self.stateChanges.clear()
504+
505+
chunk2 = b'\x00'.join([
506+
b'similarity index 100%',
507+
b'copy from guides/README.md',
508+
b'copy to .github/copilot-instructions.md',
509+
b'\x00'
510+
])
511+
512+
_, fileItems2 = self._parse(chunk2, reset=False)
513+
514+
self.assertNotIn('.github/copilot-instructions.md', fileItems2)
515+
self.assertEqual(len(self.stateChanges), 1)
516+
self.assertEqual(
517+
self.stateChanges[0][0],
518+
'.github/copilot-instructions.md'
519+
)
520+
self.assertEqual(self.stateChanges[0][1], FileState.Added)

0 commit comments

Comments
 (0)