native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #17 from onevcat/startup/direct-bundled-wt

Run bundled wt discovery directly

authored by

Wei Wang and committed by
GitHub
8a700c80 812189d5

+260 -4
+38 -4
supacode/Clients/Git/GitClient.swift
··· 59 59 nonisolated func repoRoot(for path: URL) async throws -> URL { 60 60 let normalizedPath = Self.directoryURL(for: path) 61 61 let wtURL = try wtScriptURL() 62 - let output = try await runLoginShellProcess( 62 + let output = try await runBundledWtProcess( 63 63 operation: .repoRoot, 64 64 executableURL: wtURL, 65 65 arguments: ["root"], 66 66 currentDirectoryURL: normalizedPath 67 67 ) 68 - if output.isEmpty { 68 + let trimmed = output.trimmingCharacters(in: .whitespacesAndNewlines) 69 + if trimmed.isEmpty { 69 70 let command = "\(wtURL.lastPathComponent) root" 70 71 throw GitClientError.commandFailed(command: command, message: "Empty output") 71 72 } 72 - return URL(fileURLWithPath: output).standardizedFileURL 73 + return URL(fileURLWithPath: trimmed).standardizedFileURL 73 74 } 74 75 75 76 nonisolated func worktrees(for repoRoot: URL) async throws -> [Worktree] { ··· 689 690 nonisolated private func runWtList(repoRoot: URL) async throws -> String { 690 691 let wtURL = try wtScriptURL() 691 692 let arguments = ["ls", "--json"] 692 - return try await runLoginShellProcess( 693 + return try await runBundledWtProcess( 693 694 operation: .worktreeList, 694 695 executableURL: wtURL, 695 696 arguments: arguments, ··· 702 703 fatalError("Bundled wt script not found") 703 704 } 704 705 return url 706 + } 707 + 708 + nonisolated private func runBundledWtProcess( 709 + operation: GitOperation, 710 + executableURL: URL, 711 + arguments: [String], 712 + currentDirectoryURL: URL? 713 + ) async throws -> String { 714 + let command = ([executableURL.path(percentEncoded: false)] + arguments).joined(separator: " ") 715 + do { 716 + return try await shell.run(executableURL, arguments, currentDirectoryURL).stdout 717 + } catch { 718 + guard shouldFallbackToLoginShell(error) else { 719 + throw wrapShellError(error, operation: operation, command: command) 720 + } 721 + gitLogger.info("Falling back to login shell for \(operation.rawValue)") 722 + do { 723 + return try await shell.runLogin(executableURL, arguments, currentDirectoryURL).stdout 724 + } catch { 725 + throw wrapShellError(error, operation: operation, command: command) 726 + } 727 + } 705 728 } 706 729 707 730 nonisolated private func runLoginShellProcess( ··· 845 868 } 846 869 847 870 private nonisolated let gitLogger = SupaLogger("Git") 871 + 872 + nonisolated private func shouldFallbackToLoginShell(_ error: Error) -> Bool { 873 + guard let shellError = error as? ShellClientError else { 874 + return false 875 + } 876 + if shellError.exitCode == 127 { 877 + return true 878 + } 879 + let output = "\(shellError.stderr)\n\(shellError.stdout)".lowercased() 880 + return output.contains("command not found") 881 + } 848 882 849 883 nonisolated private func wrapShellError( 850 884 _ 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 + }