diff --git a/CodeEdit/Features/Git/Client/GitClient+Branches.swift b/CodeEdit/Features/Git/Client/GitClient+Branches.swift index 187df7bda8..ac92e6b421 100644 --- a/CodeEdit/Features/Git/Client/GitClient+Branches.swift +++ b/CodeEdit/Features/Git/Client/GitClient+Branches.swift @@ -11,20 +11,28 @@ extension GitClient { /// Get branches /// - Returns: Array of branches func getBranches() async throws -> [GitBranch] { - let command = "branch --format \"%(refname:short)|%(refname)|%(upstream:short)\" -a" + let command = "branch --format \"%(refname:short)|%(refname)|%(upstream:short) %(upstream:track)\" -a" return try await run(command) .components(separatedBy: "\n") .filter { $0 != "" && !$0.contains("HEAD") } .compactMap { line in - let components = line.components(separatedBy: "|") - let name = components[0] - let upstream = components[safe: 2] + guard let branchPart = line.components(separatedBy: " ").first else { return nil } + let branchComponents = branchPart.components(separatedBy: "|") + let name = branchComponents[0] + let upstream = branchComponents[safe: 2] + + let trackInfoString = line + .dropFirst(branchPart.count) + .trimmingCharacters(in: .whitespacesWithoutNewlines) + let trackInfo = parseBranchTrackInfo(from: trackInfoString) return .init( name: name, - longName: components[safe: 1] ?? name, - upstream: upstream?.isEmpty == true ? nil : upstream + longName: branchComponents[safe: 1] ?? name, + upstream: upstream?.isEmpty == true ? nil : upstream, + ahead: trackInfo.ahead, + behind: trackInfo.behind ) } } @@ -32,18 +40,26 @@ extension GitClient { /// Get current branch func getCurrentBranch() async throws -> GitBranch? { let branchName = try await run("branch --show-current").trimmingCharacters(in: .whitespacesAndNewlines) - let components = try await run( - "for-each-ref --format=\"%(refname)|%(upstream:short)\" refs/heads/\(branchName)" + let output = try await run( + "for-each-ref --format=\"%(refname)|%(upstream:short) %(upstream:track)\" refs/heads/\(branchName)" ) .trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: "|") - let upstream = components[safe: 1] + guard let branchPart = output.components(separatedBy: " ").first else { return nil } + let branchComponents = branchPart.components(separatedBy: "|") + let upstream = branchComponents[safe: 1] + + let trackInfoString = output + .dropFirst(branchPart.count) + .trimmingCharacters(in: .whitespacesWithoutNewlines) + let trackInfo = parseBranchTrackInfo(from: trackInfoString) return .init( name: branchName, - longName: components[0], - upstream: upstream?.isEmpty == true ? nil : upstream + longName: branchComponents[0], + upstream: upstream?.isEmpty == true ? nil : upstream, + ahead: trackInfo.ahead, + behind: trackInfo.behind ) } @@ -100,4 +116,35 @@ extension GitClient { } } } + + private func parseBranchTrackInfo(from infoString: String) -> (ahead: Int, behind: Int) { + let pattern = "\\[ahead (\\d+)(?:, behind (\\d+))?\\]|\\[behind (\\d+)\\]" + // Create a regular expression object + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { + fatalError("Invalid regular expression pattern") + } + var ahead = 0 + var behind = 0 + // Match the input string with the regular expression + if let match = regex.firstMatch( + in: infoString, + options: [], + range: NSRange(location: 0, length: infoString.utf16.count) + ) { + // Extract the captured groups + if let aheadRange = Range(match.range(at: 1), in: infoString), + let aheadValue = Int(infoString[aheadRange]) { + ahead = aheadValue + } + if let behindRange = Range(match.range(at: 2), in: infoString), + let behindValue = Int(infoString[behindRange]) { + behind = behindValue + } + if let behindRange = Range(match.range(at: 3), in: infoString), + let behindValue = Int(infoString[behindRange]) { + behind = behindValue + } + } + return (ahead, behind) + } } diff --git a/CodeEdit/Features/Git/Client/Models/GitBranch.swift b/CodeEdit/Features/Git/Client/Models/GitBranch.swift index e9ea448406..338108c454 100644 --- a/CodeEdit/Features/Git/Client/Models/GitBranch.swift +++ b/CodeEdit/Features/Git/Client/Models/GitBranch.swift @@ -11,6 +11,8 @@ struct GitBranch: Hashable { let name: String let longName: String let upstream: String? + let ahead: Int + let behind: Int /// Is local branch var isLocal: Bool { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift index eb43da3347..caf40bc235 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift @@ -25,6 +25,25 @@ struct SourceControlNavigatorRepositoryItem: View { .foregroundStyle(.secondary) .font(.system(size: 11)) } + Spacer() + HStack(spacing: 5) { + if let behind = item.branch?.behind, behind > 0 { + HStack(spacing: 0) { + Image(systemName: "arrow.down") + .imageScale(.small) + Text("\(behind)") + .font(.system(size: 11)) + } + } + if let ahead = item.branch?.ahead, ahead > 0 { + HStack(spacing: 0) { + Image(systemName: "arrow.up") + .imageScale(.small) + Text("\(ahead)") + .font(.system(size: 11)) + } + } + } }, icon: { if item.symbolImage != nil { Image(symbol: item.symbolImage ?? "")