Skip to content

Commit 5c2d8ec

Browse files
Adjust Git Status Parsing to Better Handle Null Chars (#2132)
### Description Fixes an issue with git status parsing where the substring indexing was off-by-one when creating the file string due to an index being incremented before creating a substring. Fixes this by incrementing the current string index *after* returning the correct substring. The related issue is marked as a Tahoe bug, the Tahoe bug is that Tahoe now shows the null character as a %00 at the end of the label, where previous macOS versions did not. This change is retroactive however as it is a bug fix. ### Related Issues * closes #2119 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots N/A
1 parent 0abc12f commit 5c2d8ec

File tree

2 files changed

+49
-3
lines changed

2 files changed

+49
-3
lines changed

CodeEdit/Features/SourceControl/Client/GitClient+Status.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,25 @@ extension GitClient {
3636
/// - Throws: Can throw ``GitClient/GitClientError`` errors if it finds unexpected output.
3737
func getStatus() async throws -> Status {
3838
let output = try await run("status -z --porcelain=2 -u")
39+
return try parseStatusString(output)
40+
}
41+
42+
/// Parses a status string from ``getStatus()`` and returns a ``Status`` object if possible.
43+
/// - Parameter output: The git output from running `status`. Expects a porcelain v2 string.
44+
/// - Returns: A status object if parseable.
45+
func parseStatusString(_ output: borrowing String) throws -> Status {
46+
let endsInNull = output.last == Character(UnicodeScalar(0))
47+
let endIndex: String.Index
48+
if endsInNull && output.count > 1 {
49+
endIndex = output.index(before: output.endIndex)
50+
} else {
51+
endIndex = output.endIndex
52+
}
3953

4054
var status = Status(changedFiles: [], unmergedChanges: [], untrackedFiles: [])
4155

4256
var index = output.startIndex
43-
while index < output.endIndex {
57+
while index < endIndex {
4458
let typeIndex = index
4559

4660
// Move ahead no matter what.
@@ -100,7 +114,11 @@ extension GitClient {
100114
}
101115
index = newIndex
102116
}
103-
index = output.index(after: index)
117+
defer {
118+
if index < output.index(before: output.endIndex) {
119+
index = output.index(after: index)
120+
}
121+
}
104122
return output[startIndex..<index]
105123
}
106124

@@ -147,7 +165,8 @@ extension GitClient {
147165
try moveToNextSpace(from: &index, output: output)
148166
}
149167
try moveOneChar(from: &index, output: output)
150-
let filename = String(try substringToNextNull(from: &index, output: output))
168+
let substring = try substringToNextNull(from: &index, output: output)
169+
let filename = String(substring)
151170
return GitChangedFile(
152171
status: status,
153172
stagedStatus: stagedStatus,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// GitClientTests.swift
3+
// CodeEditTests
4+
//
5+
// Created by Khan Winter on 9/11/25.
6+
//
7+
8+
import Testing
9+
@testable import CodeEdit
10+
11+
@Suite
12+
struct GitClientTests {
13+
@Test
14+
func statusParseNullAtEnd() throws {
15+
try withTempDir { dirURL in
16+
// swiftlint:disable:next line_length
17+
let string = "1 .M N... 100644 100644 100644 eaef31cfa2a22418c00d7477da0b7151d122681e eaef31cfa2a22418c00d7477da0b7151d122681e CodeEdit/Features/SourceControl/Client/GitClient+Status.swift\01 AM N... 000000 100644 100644 0000000000000000000000000000000000000000 e0f5ce250b32cf6610a284b7a33ac114079f5159 CodeEditTests/Features/SourceControl/GitClientTests.swift\0"
18+
let client = GitClient(directoryURL: dirURL, shellClient: .live())
19+
let status = try client.parseStatusString(string)
20+
21+
#expect(status.changedFiles.count == 2)
22+
// No null string at the end
23+
#expect(status.changedFiles[0].fileURL.lastPathComponent == "GitClient+Status.swift")
24+
#expect(status.changedFiles[1].fileURL.lastPathComponent == "GitClientTests.swift")
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)