From 63c73a19e0b484490ad48027227832f4169418a1 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 00:07:45 +0000 Subject: [PATCH 1/4] feat: store git identity in Keychain and export author/committer env --- Sources/GitwCore/GitwApp.swift | 4 +++- Sources/GitwCore/Keychain.swift | 10 ++++---- .../GitEnvironmentBuilderTests.swift | 24 +++++++++---------- Tests/GitwCoreTests/GitwAppTests.swift | 11 +++++---- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/Sources/GitwCore/GitwApp.swift b/Sources/GitwCore/GitwApp.swift index f100588..f75d142 100644 --- a/Sources/GitwCore/GitwApp.swift +++ b/Sources/GitwCore/GitwApp.swift @@ -78,7 +78,9 @@ public struct GitwApp { throw GitwError.usage("login requires --email ") } - let username = try ttyReadLine("GitHub username: ") + // `--name` is the GitHub username (and also the commit author name we pass via env). + // Do not prompt for username again. + let username = name let token = try ttyReadSecret("GitHub personal access token: ") let profile = GitwProfile(githubUsername: username, token: token, gitName: name, gitEmail: email) diff --git a/Sources/GitwCore/Keychain.swift b/Sources/GitwCore/Keychain.swift index e700998..98aecc9 100644 --- a/Sources/GitwCore/Keychain.swift +++ b/Sources/GitwCore/Keychain.swift @@ -30,8 +30,9 @@ public struct GitwProfile: Sendable, Codable, Equatable { } public enum KeychainStore { - // We deliberately keep this fixed; gitw only supports GitHub HTTPS. - public static let server = "github.com" + // Keychain namespace. We still authenticate against github.com, but we store credentials + // under a dedicated Keychain server name to avoid collisions with other GitHub tooling. + public static let server = "gitw.github.com" private static let service = "gitw" /// Load credentials for a given alias. @@ -74,11 +75,10 @@ public enum KeychainStore { } } - // We expect profile JSON for all entries in this design. - // If it's missing, fail closed. + // Name/email not present in legacy entries; we fail closed because the design now requires them. let username = (dict[kSecAttrComment as String] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let hint = (username?.isEmpty == false) ? username! : accountAlias - throw GitwError.keychain("profile for alias \(accountAlias) is missing profile metadata (expected JSON). Please re-run login for this alias (github username was \(hint)).") + throw GitwError.keychain("profile for alias \(accountAlias) is missing name/email (legacy entry). Please re-run: gitw login --as \(accountAlias) --name ... --email ... (github username was \(hint))") } /// Save credentials under a local alias. diff --git a/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift b/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift index 3fcd5be..1d28116 100644 --- a/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift +++ b/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift @@ -36,19 +36,19 @@ struct GitEnvironmentBuilderTests { let env = GitRunner.buildGitEnvironment( base: [:], - askpassPath: askpass, - brokerSocket: sock, - brokerNonce: nonce, - profile: GitwProfile(githubUsername: "u-\(id)", token: "t-\(id)", gitName: name, gitEmail: email) + askpassPath: "/usr/local/bin/gitw-askpass", + brokerSocket: "/tmp/sock", + brokerNonce: "nonce", + profile: GitwProfile(githubUsername: "u", token: "t", gitName: "N", gitEmail: "e@example.com") ) - #expect(env["GIT_ASKPASS"] == askpass) - #expect(env["SSH_ASKPASS"] == askpass) - #expect(env["GITW_SOCKET"] == sock) - #expect(env["GITW_NONCE"] == nonce) - #expect(env["GIT_AUTHOR_NAME"] == name) - #expect(env["GIT_AUTHOR_EMAIL"] == email) - #expect(env["GIT_COMMITTER_NAME"] == name) - #expect(env["GIT_COMMITTER_EMAIL"] == email) + #expect(env["GIT_ASKPASS"] == "/usr/local/bin/gitw-askpass") + #expect(env["SSH_ASKPASS"] == "/usr/local/bin/gitw-askpass") + #expect(env["GITW_SOCKET"] == "/tmp/sock") + #expect(env["GITW_NONCE"] == "nonce") + #expect(env["GIT_AUTHOR_NAME"] == "N") + #expect(env["GIT_AUTHOR_EMAIL"] == "e@example.com") + #expect(env["GIT_COMMITTER_NAME"] == "N") + #expect(env["GIT_COMMITTER_EMAIL"] == "e@example.com") } } diff --git a/Tests/GitwCoreTests/GitwAppTests.swift b/Tests/GitwCoreTests/GitwAppTests.swift index 19bf7d5..4d29671 100644 --- a/Tests/GitwCoreTests/GitwAppTests.swift +++ b/Tests/GitwCoreTests/GitwAppTests.swift @@ -59,9 +59,9 @@ struct GitwAppTests { let status = try app.run( .login(alias: "work", repoURL: "https://github.com/OWNER/REPO.git"), - ttyReadLine: { _ in "real-user" }, + ttyReadLine: { _ in throw GitwError.io("unexpected ttyReadLine") }, ttyReadSecret: { _ in "tok" }, - name: "Real Name", + name: "real-user", email: "real@example.com" ) @@ -69,11 +69,12 @@ struct GitwAppTests { #expect(git.calls.count == 1) #expect(git.calls[0].args == ["ls-remote", "https://github.com/OWNER/REPO.git"]) #expect(git.calls[0].githubUsername == "real-user") - #expect(git.calls[0].name == "Real Name") + #expect(git.calls[0].name == "real-user") #expect(git.calls[0].email == "real@example.com") #expect(kc.saved.count == 1) #expect(kc.saved[0].alias == "work") #expect(kc.saved[0].profile.githubUsername == "real-user") + #expect(kc.saved[0].profile.gitName == "real-user") } @Test @@ -87,9 +88,9 @@ struct GitwAppTests { do { _ = try app.run( .login(alias: "work", repoURL: "https://github.com/OWNER/REPO.git"), - ttyReadLine: { _ in "real-user" }, + ttyReadLine: { _ in throw GitwError.io("unexpected ttyReadLine") }, ttyReadSecret: { _ in "tok" }, - name: "Real Name", + name: "real-user", email: "real@example.com" ) Issue.record("Expected login to fail") From 3c6431fb800569c73ed1ed516cb2b0e8dc713a9d Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 00:14:47 +0000 Subject: [PATCH 2/4] test: randomize env-builder expected values --- .../GitEnvironmentBuilderTests.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift b/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift index 1d28116..3fcd5be 100644 --- a/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift +++ b/Tests/GitwCoreTests/GitEnvironmentBuilderTests.swift @@ -36,19 +36,19 @@ struct GitEnvironmentBuilderTests { let env = GitRunner.buildGitEnvironment( base: [:], - askpassPath: "/usr/local/bin/gitw-askpass", - brokerSocket: "/tmp/sock", - brokerNonce: "nonce", - profile: GitwProfile(githubUsername: "u", token: "t", gitName: "N", gitEmail: "e@example.com") + askpassPath: askpass, + brokerSocket: sock, + brokerNonce: nonce, + profile: GitwProfile(githubUsername: "u-\(id)", token: "t-\(id)", gitName: name, gitEmail: email) ) - #expect(env["GIT_ASKPASS"] == "/usr/local/bin/gitw-askpass") - #expect(env["SSH_ASKPASS"] == "/usr/local/bin/gitw-askpass") - #expect(env["GITW_SOCKET"] == "/tmp/sock") - #expect(env["GITW_NONCE"] == "nonce") - #expect(env["GIT_AUTHOR_NAME"] == "N") - #expect(env["GIT_AUTHOR_EMAIL"] == "e@example.com") - #expect(env["GIT_COMMITTER_NAME"] == "N") - #expect(env["GIT_COMMITTER_EMAIL"] == "e@example.com") + #expect(env["GIT_ASKPASS"] == askpass) + #expect(env["SSH_ASKPASS"] == askpass) + #expect(env["GITW_SOCKET"] == sock) + #expect(env["GITW_NONCE"] == nonce) + #expect(env["GIT_AUTHOR_NAME"] == name) + #expect(env["GIT_AUTHOR_EMAIL"] == email) + #expect(env["GIT_COMMITTER_NAME"] == name) + #expect(env["GIT_COMMITTER_EMAIL"] == email) } } From ecd9ab8465b6074a4b03c2ecbc732f86ad953e9b Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 00:25:12 +0000 Subject: [PATCH 3/4] refactor: require Keychain profile (no legacy/optional) --- Sources/GitwCore/Keychain.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/GitwCore/Keychain.swift b/Sources/GitwCore/Keychain.swift index 98aecc9..e1b2851 100644 --- a/Sources/GitwCore/Keychain.swift +++ b/Sources/GitwCore/Keychain.swift @@ -75,10 +75,11 @@ public enum KeychainStore { } } - // Name/email not present in legacy entries; we fail closed because the design now requires them. + // We expect profile JSON for all entries in this design. + // If it's missing, fail closed. let username = (dict[kSecAttrComment as String] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let hint = (username?.isEmpty == false) ? username! : accountAlias - throw GitwError.keychain("profile for alias \(accountAlias) is missing name/email (legacy entry). Please re-run: gitw login --as \(accountAlias) --name ... --email ... (github username was \(hint))") + throw GitwError.keychain("profile for alias \(accountAlias) is missing profile metadata (expected JSON). Please re-run login for this alias (github username was \(hint)).") } /// Save credentials under a local alias. From 60a2018570bc226fbd7f75e1f84e1b3897247218 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 11:50:11 +0000 Subject: [PATCH 4/4] Add default CODEOWNERS --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d3a3d86 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default code owners +* @martin-alexander-msc