Skip to content

Commit ff10bb6

Browse files
committed
fix(difffetcher): preserve incremental rename/copy state across chunks
Keep persisted file state when later metadata like index/new mode arrives in a separate parse() call, so rename targets become RenamedModified instead of Modified and copy targets stay Added. Add regression tests for split rename/index and copy/index scenarios.
1 parent 9389ef3 commit ff10bb6

File tree

2 files changed

+95
-8
lines changed

2 files changed

+95
-8
lines changed

qgitc/difffetcher.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,31 +132,39 @@ def _updateFileState():
132132
continue
133133
else:
134134
itemType = DiffType.FileInfo
135+
fileToUpdate = fullDisplayFileStr or fullFileBStr or \
136+
fullFileAStr or self._currentFileB or self._currentFileA
137+
currentState = fileState
138+
if currentState == FileState.Normal and fileToUpdate:
139+
currentState = self._fileStates.get(
140+
fileToUpdate, FileState.Normal)
141+
135142
if line.startswith(b"new file mode "):
136143
fileState = FileState.Added
137144
elif line.startswith(b"deleted file mode "):
138145
fileState = FileState.Deleted
139146
elif line.startswith(b"new mode "):
140-
if fileState == FileState.Normal:
147+
fileState = currentState
148+
if currentState == FileState.Normal:
141149
fileState = FileState.Modified
142-
elif fileState == FileState.Renamed:
150+
elif currentState == FileState.Renamed:
143151
fileState = FileState.RenamedModified
144152
elif line.startswith(b"rename "):
145-
if fileState == FileState.Normal:
153+
fileState = currentState
154+
if currentState == FileState.Normal:
146155
fileState = FileState.Renamed
147-
elif fileState == FileState.Modified:
156+
elif currentState == FileState.Modified:
148157
fileState = FileState.RenamedModified
149158
elif line.startswith(b"copy to "):
150159
fileState = FileState.Added
151160
elif line.startswith(b"index "):
152-
if fileState == FileState.Renamed:
161+
fileState = currentState
162+
if currentState == FileState.Renamed:
153163
fileState = FileState.RenamedModified
154-
elif fileState == FileState.Normal:
164+
elif currentState == FileState.Normal:
155165
fileState = FileState.Modified
156166

157167
if fileState != FileState.Normal:
158-
fileToUpdate = fullDisplayFileStr or fullFileBStr or \
159-
fullFileAStr or self._currentFileB or self._currentFileA
160168
if fileToUpdate:
161169
oldState = self._fileStates.get(fileToUpdate)
162170
self._fileStates[fileToUpdate] = fileState

tests/test_difffetcher_bugs.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,82 @@ def test_true_incremental_copy_metadata_updates_target_state(self):
518518
'.github/copilot-instructions.md'
519519
)
520520
self.assertEqual(self.stateChanges[0][1], FileState.Added)
521+
522+
def test_true_incremental_rename_index_keeps_renamed_modified(self):
523+
"""rename metadata in one chunk and index in the next should yield RenamedModified."""
524+
chunk1 = b'\x00'.join([
525+
b'diff --git a/old.txt b/new.txt',
526+
b'\x00'
527+
])
528+
529+
_, fileItems1 = self._parse(chunk1)
530+
self.assertIn('new.txt', fileItems1)
531+
self.assertEqual(fileItems1['new.txt'].state, FileState.Normal)
532+
533+
self.stateChanges.clear()
534+
535+
chunk2 = b'\x00'.join([
536+
b'rename from old.txt',
537+
b'rename to new.txt',
538+
b'\x00'
539+
])
540+
541+
_, fileItems2 = self._parse(chunk2, reset=False)
542+
543+
self.assertNotIn('new.txt', fileItems2)
544+
self.assertEqual(len(self.stateChanges), 1)
545+
self.assertEqual(self.stateChanges[0], ('new.txt', FileState.Renamed))
546+
547+
self.stateChanges.clear()
548+
549+
chunk3 = b'\x00'.join([
550+
b'index abc123..def456 100644',
551+
b'\x00'
552+
])
553+
554+
_, fileItems3 = self._parse(chunk3, reset=False)
555+
556+
self.assertNotIn('new.txt', fileItems3)
557+
self.assertEqual(len(self.stateChanges), 1)
558+
self.assertEqual(
559+
self.stateChanges[0],
560+
('new.txt', FileState.RenamedModified)
561+
)
562+
563+
def test_true_incremental_copy_index_does_not_downgrade_added(self):
564+
"""copy metadata in one chunk and index in the next should remain Added."""
565+
chunk1 = b'\x00'.join([
566+
b'diff --git a/src.txt b/dst.txt',
567+
b'\x00'
568+
])
569+
570+
_, fileItems1 = self._parse(chunk1)
571+
self.assertIn('dst.txt', fileItems1)
572+
self.assertEqual(fileItems1['dst.txt'].state, FileState.Normal)
573+
574+
self.stateChanges.clear()
575+
576+
chunk2 = b'\x00'.join([
577+
b'similarity index 100%',
578+
b'copy from src.txt',
579+
b'copy to dst.txt',
580+
b'\x00'
581+
])
582+
583+
_, fileItems2 = self._parse(chunk2, reset=False)
584+
585+
self.assertNotIn('dst.txt', fileItems2)
586+
self.assertEqual(len(self.stateChanges), 1)
587+
self.assertEqual(self.stateChanges[0], ('dst.txt', FileState.Added))
588+
589+
self.stateChanges.clear()
590+
591+
chunk3 = b'\x00'.join([
592+
b'index abc123..def456 100644',
593+
b'\x00'
594+
])
595+
596+
_, fileItems3 = self._parse(chunk3, reset=False)
597+
598+
self.assertNotIn('dst.txt', fileItems3)
599+
self.assertEqual(self.stateChanges, [])

0 commit comments

Comments
 (0)