native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #161 from onevcat/contrib/direct-bundled-wt

Run bundled wt discovery directly

authored by

khoi and committed by
GitHub
5e705cf7 452cdec3

+260 -4
+38 -4
supacode/Clients/Git/GitClient.swift
··· 56 56 nonisolated func repoRoot(for path: URL) async throws -> URL { 57 57 let normalizedPath = Self.directoryURL(for: path) 58 58 let wtURL = try wtScriptURL() 59 - let output = try await runLoginShellProcess( 59 + let output = try await runBundledWtProcess( 60 60 operation: .repoRoot, 61 61 executableURL: wtURL, 62 62 arguments: ["root"], 63 63 currentDirectoryURL: normalizedPath 64 64 ) 65 - if output.isEmpty { 65 + let trimmed = output.trimmingCharacters(in: .whitespacesAndNewlines) 66 + if trimmed.isEmpty { 66 67 let command = "\(wtURL.lastPathComponent) root" 67 68 throw GitClientError.commandFailed(command: command, message: "Empty output") 68 69 } 69 - return URL(fileURLWithPath: output).standardizedFileURL 70 + return URL(fileURLWithPath: trimmed).standardizedFileURL 70 71 } 71 72 72 73 nonisolated func worktrees(for repoRoot: URL) async throws -> [Worktree] { ··· 645 646 nonisolated private func runWtList(repoRoot: URL) async throws -> String { 646 647 let wtURL = try wtScriptURL() 647 648 let arguments = ["ls", "--json"] 648 - return try await runLoginShellProcess( 649 + return try await runBundledWtProcess( 649 650 operation: .worktreeList, 650 651 executableURL: wtURL, 651 652 arguments: arguments, ··· 658 659 fatalError("Bundled wt script not found") 659 660 } 660 661 return url 662 + } 663 + 664 + nonisolated private func runBundledWtProcess( 665 + operation: GitOperation, 666 + executableURL: URL, 667 + arguments: [String], 668 + currentDirectoryURL: URL? 669 + ) async throws -> String { 670 + let command = ([executableURL.path(percentEncoded: false)] + arguments).joined(separator: " ") 671 + do { 672 + return try await shell.run(executableURL, arguments, currentDirectoryURL).stdout 673 + } catch { 674 + guard shouldFallbackToLoginShell(error) else { 675 + throw wrapShellError(error, operation: operation, command: command) 676 + } 677 + gitLogger.info("Falling back to login shell for \(operation.rawValue)") 678 + do { 679 + return try await shell.runLogin(executableURL, arguments, currentDirectoryURL).stdout 680 + } catch { 681 + throw wrapShellError(error, operation: operation, command: command) 682 + } 683 + } 661 684 } 662 685 663 686 nonisolated private func runLoginShellProcess( ··· 801 824 } 802 825 803 826 private nonisolated let gitLogger = SupaLogger("Git") 827 + 828 + nonisolated private func shouldFallbackToLoginShell(_ error: Error) -> Bool { 829 + guard let shellError = error as? ShellClientError else { 830 + return false 831 + } 832 + if shellError.exitCode == 127 { 833 + return true 834 + } 835 + let output = "\(shellError.stderr)\n\(shellError.stdout)".lowercased() 836 + return output.contains("command not found") 837 + } 804 838 805 839 nonisolated private func wrapShellError( 806 840 _ error: Error,
+222
supacodeTests/GitClientWorktreeDiscoveryTests.swift
··· 1 + import Foundation 2 + import Testing 3 + 4 + @testable import supacode 5 + 6 + nonisolated final class GitWorktreeDiscoveryRecorder: @unchecked Sendable { 7 + struct Invocation: Equatable { 8 + let executablePath: String 9 + let arguments: [String] 10 + let currentDirectoryPath: String? 11 + } 12 + 13 + private let lock = NSLock() 14 + private var runInvocationsValue: [Invocation] = [] 15 + private var loginInvocationsValue: [Invocation] = [] 16 + 17 + func recordRun(executableURL: URL, arguments: [String], currentDirectoryURL: URL?) { 18 + lock.lock() 19 + runInvocationsValue.append( 20 + Invocation( 21 + executablePath: executableURL.path(percentEncoded: false), 22 + arguments: arguments, 23 + currentDirectoryPath: currentDirectoryURL?.path(percentEncoded: false) 24 + ) 25 + ) 26 + lock.unlock() 27 + } 28 + 29 + func recordLogin(executableURL: URL, arguments: [String], currentDirectoryURL: URL?) { 30 + lock.lock() 31 + loginInvocationsValue.append( 32 + Invocation( 33 + executablePath: executableURL.path(percentEncoded: false), 34 + arguments: arguments, 35 + currentDirectoryPath: currentDirectoryURL?.path(percentEncoded: false) 36 + ) 37 + ) 38 + lock.unlock() 39 + } 40 + 41 + func runInvocations() -> [Invocation] { 42 + lock.lock() 43 + let value = runInvocationsValue 44 + lock.unlock() 45 + return value 46 + } 47 + 48 + func loginInvocations() -> [Invocation] { 49 + lock.lock() 50 + let value = loginInvocationsValue 51 + lock.unlock() 52 + return value 53 + } 54 + } 55 + 56 + struct GitClientWorktreeDiscoveryTests { 57 + @Test func repoRootUsesDirectBundledWtExecution() async throws { 58 + let recorder = GitWorktreeDiscoveryRecorder() 59 + let shell = ShellClient( 60 + run: { executableURL, arguments, currentDirectoryURL in 61 + recorder.recordRun( 62 + executableURL: executableURL, 63 + arguments: arguments, 64 + currentDirectoryURL: currentDirectoryURL 65 + ) 66 + return ShellOutput(stdout: "/tmp/repo\n", stderr: "", exitCode: 0) 67 + }, 68 + runLoginImpl: { executableURL, arguments, currentDirectoryURL, _ in 69 + recorder.recordLogin( 70 + executableURL: executableURL, 71 + arguments: arguments, 72 + currentDirectoryURL: currentDirectoryURL 73 + ) 74 + Issue.record("repoRoot should not use runLogin when direct execution succeeds") 75 + return ShellOutput(stdout: "", stderr: "", exitCode: 0) 76 + } 77 + ) 78 + let client = GitClient(shell: shell) 79 + let worktreeURL = URL(fileURLWithPath: "/tmp/repo/worktree") 80 + 81 + let root = try await client.repoRoot(for: worktreeURL) 82 + 83 + #expect(root.standardizedFileURL.path(percentEncoded: false).hasSuffix("/tmp/repo")) 84 + let runs = recorder.runInvocations() 85 + #expect(runs.count == 1) 86 + if let invocation = runs.first { 87 + #expect(invocation.arguments == ["root"]) 88 + let normalizedPath = URL(fileURLWithPath: invocation.currentDirectoryPath ?? "") 89 + .standardizedFileURL 90 + .path(percentEncoded: false) 91 + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) 92 + #expect(normalizedPath == "tmp/repo") 93 + } else { 94 + Issue.record("Expected one direct bundled wt invocation for repoRoot") 95 + } 96 + #expect(recorder.loginInvocations().isEmpty) 97 + } 98 + 99 + @Test func worktreesUseDirectBundledWtExecution() async throws { 100 + let recorder = GitWorktreeDiscoveryRecorder() 101 + let output = """ 102 + [ 103 + {"branch":"main","path":"/tmp/repo","head":"abc","is_bare":false}, 104 + {"branch":"feature","path":"/tmp/repo/.worktrees/feature","head":"def","is_bare":false} 105 + ] 106 + """ 107 + let shell = ShellClient( 108 + run: { executableURL, arguments, currentDirectoryURL in 109 + recorder.recordRun( 110 + executableURL: executableURL, 111 + arguments: arguments, 112 + currentDirectoryURL: currentDirectoryURL 113 + ) 114 + return ShellOutput(stdout: output, stderr: "", exitCode: 0) 115 + }, 116 + runLoginImpl: { executableURL, arguments, currentDirectoryURL, _ in 117 + recorder.recordLogin( 118 + executableURL: executableURL, 119 + arguments: arguments, 120 + currentDirectoryURL: currentDirectoryURL 121 + ) 122 + Issue.record("worktrees should not use runLogin when direct execution succeeds") 123 + return ShellOutput(stdout: "", stderr: "", exitCode: 0) 124 + } 125 + ) 126 + let client = GitClient(shell: shell) 127 + let repoRoot = URL(fileURLWithPath: "/tmp/repo") 128 + 129 + let worktrees = try await client.worktrees(for: repoRoot) 130 + 131 + #expect(worktrees.map(\.id) == ["/tmp/repo", "/tmp/repo/.worktrees/feature"]) 132 + let runs = recorder.runInvocations() 133 + #expect(runs.count == 1) 134 + if let invocation = runs.first { 135 + #expect(invocation.arguments == ["ls", "--json"]) 136 + #expect(invocation.currentDirectoryPath == "/tmp/repo") 137 + } else { 138 + Issue.record("Expected one direct bundled wt invocation for worktree discovery") 139 + } 140 + #expect(recorder.loginInvocations().isEmpty) 141 + } 142 + 143 + @Test func repoRootFallsBackToLoginShellWhenDirectExecutionCannotResolveGit() async throws { 144 + let recorder = GitWorktreeDiscoveryRecorder() 145 + let shell = ShellClient( 146 + run: { executableURL, arguments, currentDirectoryURL in 147 + recorder.recordRun( 148 + executableURL: executableURL, 149 + arguments: arguments, 150 + currentDirectoryURL: currentDirectoryURL 151 + ) 152 + throw ShellClientError( 153 + command: "wt root", 154 + stdout: "", 155 + stderr: "git: command not found", 156 + exitCode: 127 157 + ) 158 + }, 159 + runLoginImpl: { executableURL, arguments, currentDirectoryURL, _ in 160 + recorder.recordLogin( 161 + executableURL: executableURL, 162 + arguments: arguments, 163 + currentDirectoryURL: currentDirectoryURL 164 + ) 165 + return ShellOutput(stdout: "/tmp/repo\n", stderr: "", exitCode: 0) 166 + } 167 + ) 168 + let client = GitClient(shell: shell) 169 + 170 + let root = try await client.repoRoot(for: URL(fileURLWithPath: "/tmp/repo/worktree")) 171 + 172 + #expect(root.standardizedFileURL.path(percentEncoded: false).hasSuffix("/tmp/repo")) 173 + #expect(recorder.runInvocations().count == 1) 174 + #expect(recorder.loginInvocations().count == 1) 175 + if let invocation = recorder.loginInvocations().first { 176 + #expect(invocation.arguments == ["root"]) 177 + let normalizedPath = URL(fileURLWithPath: invocation.currentDirectoryPath ?? "") 178 + .standardizedFileURL 179 + .path(percentEncoded: false) 180 + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) 181 + #expect(normalizedPath == "tmp/repo") 182 + } else { 183 + Issue.record("Expected login-shell fallback invocation for repoRoot") 184 + } 185 + } 186 + 187 + @Test func worktreesDoNotFallbackToLoginShellForRegularFailures() async { 188 + let recorder = GitWorktreeDiscoveryRecorder() 189 + let shell = ShellClient( 190 + run: { executableURL, arguments, currentDirectoryURL in 191 + recorder.recordRun( 192 + executableURL: executableURL, 193 + arguments: arguments, 194 + currentDirectoryURL: currentDirectoryURL 195 + ) 196 + throw ShellClientError( 197 + command: "wt ls --json", 198 + stdout: "", 199 + stderr: "permission denied", 200 + exitCode: 1 201 + ) 202 + }, 203 + runLoginImpl: { executableURL, arguments, currentDirectoryURL, _ in 204 + recorder.recordLogin( 205 + executableURL: executableURL, 206 + arguments: arguments, 207 + currentDirectoryURL: currentDirectoryURL 208 + ) 209 + Issue.record("worktrees should not fallback to runLogin for regular command failures") 210 + return ShellOutput(stdout: "", stderr: "", exitCode: 0) 211 + } 212 + ) 213 + let client = GitClient(shell: shell) 214 + 215 + await #expect(throws: GitClientError.self) { 216 + _ = try await client.worktrees(for: URL(fileURLWithPath: "/tmp/repo")) 217 + } 218 + 219 + #expect(recorder.runInvocations().count == 1) 220 + #expect(recorder.loginInvocations().isEmpty) 221 + } 222 + }