From 44a26ca973fe0a4de39e4c71988557ed6645a4db Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 00:07:45 +0000 Subject: [PATCH 1/7] feat: store git identity in Keychain and export author/committer env --- Sources/GitwCore/Keychain.swift | 5 ++-- .../GitEnvironmentBuilderTests.swift | 24 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Sources/GitwCore/Keychain.swift b/Sources/GitwCore/Keychain.swift index e1b2851..98aecc9 100644 --- a/Sources/GitwCore/Keychain.swift +++ b/Sources/GitwCore/Keychain.swift @@ -75,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") } } From 2fb4d2dc4f351682c42fe17635814e37c6134fc1 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 00:14:47 +0000 Subject: [PATCH 2/7] 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 ccd83f5a6e7523769f2d8e2114c9a3d510b27af6 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 00:25:12 +0000 Subject: [PATCH 3/7] 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 006ddface2e61c4eb6f9a0967e4cd3a6d9d7f7b0 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 00:07:45 +0000 Subject: [PATCH 4/7] feat: store git identity in Keychain and export author/committer env --- Sources/GitwCore/Keychain.swift | 5 ++-- .../GitEnvironmentBuilderTests.swift | 24 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Sources/GitwCore/Keychain.swift b/Sources/GitwCore/Keychain.swift index e1b2851..98aecc9 100644 --- a/Sources/GitwCore/Keychain.swift +++ b/Sources/GitwCore/Keychain.swift @@ -75,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") } } From ee6e122c3726b4464804eb6f7849e8f83cae96a8 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 12:08:15 +0000 Subject: [PATCH 5/7] Fix Keychain profile storage (generic password JSON) --- Sources/GitwCore/Keychain.swift | 75 +++++++++++---------------------- Sources/gitw/main.swift | 2 +- 2 files changed, 26 insertions(+), 51 deletions(-) diff --git a/Sources/GitwCore/Keychain.swift b/Sources/GitwCore/Keychain.swift index 98aecc9..a7f1120 100644 --- a/Sources/GitwCore/Keychain.swift +++ b/Sources/GitwCore/Keychain.swift @@ -30,24 +30,21 @@ public struct GitwProfile: Sendable, Codable, Equatable { } public enum KeychainStore { - // 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" + // Keychain namespace. We store the full profile as a single Keychain item. + // Use a dedicated service name to avoid collisions with other tooling. + public static let service = "gitw.github.com" /// Load credentials for a given alias. /// /// - alias: Local selector key. Not necessarily the GitHub username. public static func load(alias: String) throws -> GitwProfile? { let query: [String: Any] = [ - kSecClass as String: kSecClassInternetPassword, - kSecAttrServer as String: server, - kSecAttrProtocol as String: kSecAttrProtocolHTTPS, + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, kSecAttrAccount as String: alias, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, - kSecReturnData as String: true, - kSecAttrLabel as String: service + kSecReturnData as String: true ] var item: CFTypeRef? @@ -59,62 +56,42 @@ public enum KeychainStore { guard let dict = item as? [String: Any], let accountAlias = dict[kSecAttrAccount as String] as? String, - let data = dict[kSecValueData as String] as? Data, - let _ = String(data: data, encoding: .utf8) + let data = dict[kSecValueData as String] as? Data else { throw GitwError.keychain("unexpected keychain item shape") } - // Primary storage: JSON in kSecAttrGeneric. - if let generic = dict[kSecAttrGeneric as String] as? Data { - do { - let p = try JSONDecoder().decode(GitwProfile.self, from: generic) - return p - } catch { - throw GitwError.keychain("failed to decode profile JSON: \(error)") - } + do { + return try JSONDecoder().decode(GitwProfile.self, from: data) + } catch { + // Fail closed: profile is required for gitw to operate. + throw GitwError.keychain("profile for alias \(accountAlias) is missing profile metadata (expected JSON). Please re-run login for this alias.") } - - // 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 name/email (legacy entry). Please re-run: gitw login --as \(accountAlias) --name ... --email ... (github username was \(hint))") } /// Save credentials under a local alias. public static func save(alias: String, profile: GitwProfile) throws { - let tokenData = Data(profile.token.utf8) - let generic = try JSONEncoder().encode(profile) + let json = try JSONEncoder().encode(profile) let attrs: [String: Any] = [ - kSecClass as String: kSecClassInternetPassword, - kSecAttrServer as String: server, - kSecAttrProtocol as String: kSecAttrProtocolHTTPS, + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, // Account is the selector alias. kSecAttrAccount as String: alias, - // Store actual GitHub username separately (also kept for human inspection). - kSecAttrComment as String: profile.githubUsername, - // Store full profile JSON. - kSecAttrGeneric as String: generic, - // Secret token stays as value data. - kSecValueData as String: tokenData, - kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, - kSecAttrLabel as String: service + // Full profile JSON stored as the secret payload. + kSecValueData as String: json, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock ] let status = SecItemAdd(attrs as CFDictionary, nil) if status == errSecDuplicateItem { let query: [String: Any] = [ - kSecClass as String: kSecClassInternetPassword, - kSecAttrServer as String: server, - kSecAttrProtocol as String: kSecAttrProtocolHTTPS, - kSecAttrAccount as String: alias, - kSecAttrLabel as String: service + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: alias ] let update: [String: Any] = [ - kSecAttrComment as String: profile.githubUsername, - kSecAttrGeneric as String: generic, - kSecValueData as String: tokenData + kSecValueData as String: json ] let s2 = SecItemUpdate(query as CFDictionary, update as CFDictionary) guard s2 == errSecSuccess else { @@ -129,11 +106,9 @@ public enum KeychainStore { public static func delete(alias: String) throws { let query: [String: Any] = [ - kSecClass as String: kSecClassInternetPassword, - kSecAttrServer as String: server, - kSecAttrProtocol as String: kSecAttrProtocolHTTPS, - kSecAttrAccount as String: alias, - kSecAttrLabel as String: service + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: alias ] let status = SecItemDelete(query as CFDictionary) if status == errSecItemNotFound { return } diff --git a/Sources/gitw/main.swift b/Sources/gitw/main.swift index f927071..fc8066f 100644 --- a/Sources/gitw/main.swift +++ b/Sources/gitw/main.swift @@ -88,7 +88,7 @@ do { exit(0) case "logout": _ = try app.run(.logout(alias: alias), ttyReadLine: TTY.readLine(prompt:), ttyReadSecret: TTY.readSecret(prompt:)) - print("Deleted GitHub credentials for alias \(alias) (\(KeychainStore.server)) from Keychain.") + print("Deleted GitHub credentials for alias \(alias) (\(KeychainStore.service)) from Keychain.") exit(0) case "login": // login requires a repo URL plus identity fields stored in Keychain. From 627d44b3ef1db85b1809d80842e54af325035669 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 12:14:38 +0000 Subject: [PATCH 6/7] Improve Keychain errors and prompt on duplicate overwrite --- Sources/GitwCore/GitwApp.swift | 24 ++++++++++++++++++++---- Sources/GitwCore/Keychain.swift | 24 +++++++++++++++++++----- Tests/GitwCoreTests/GitwAppTests.swift | 5 ++++- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Sources/GitwCore/GitwApp.swift b/Sources/GitwCore/GitwApp.swift index f75d142..d1a3426 100644 --- a/Sources/GitwCore/GitwApp.swift +++ b/Sources/GitwCore/GitwApp.swift @@ -2,7 +2,9 @@ import Foundation public protocol KeychainProviding { func load(alias: String) throws -> GitwProfile? - func save(alias: String, profile: GitwProfile) throws + /// Save a profile under alias. + /// - overwrite: if false, this must fail if the item already exists. + func save(alias: String, profile: GitwProfile, overwrite: Bool) throws func delete(alias: String) throws } @@ -17,8 +19,8 @@ public struct RealKeychainProvider: KeychainProviding { try KeychainStore.load(alias: alias) } - public func save(alias: String, profile: GitwProfile) throws { - try KeychainStore.save(alias: alias, profile: profile) + public func save(alias: String, profile: GitwProfile, overwrite: Bool) throws { + try KeychainStore.save(alias: alias, profile: profile, overwrite: overwrite) } public func delete(alias: String) throws { @@ -95,7 +97,21 @@ public struct GitwApp { throw GitwError.io("Login check failed (git exit \(status)). Not saved.") } - try keychain.save(alias: alias, profile: profile) + do { + try keychain.save(alias: alias, profile: profile, overwrite: false) + } catch let e as GitwError { + // If the key already exists, ask whether to overwrite. + if case .keychain(let msg) = e, msg.contains("already exists") { + let ans = try ttyReadLine("Keychain item already exists for alias \(alias). Override? (y/N): ") + if ans.lowercased().hasPrefix("y") { + try keychain.save(alias: alias, profile: profile, overwrite: true) + } else { + throw GitwError.denied("Aborted.") + } + } else { + throw e + } + } return 0 case .git(let alias, let args): diff --git a/Sources/GitwCore/Keychain.swift b/Sources/GitwCore/Keychain.swift index a7f1120..d9203be 100644 --- a/Sources/GitwCore/Keychain.swift +++ b/Sources/GitwCore/Keychain.swift @@ -34,6 +34,17 @@ public enum KeychainStore { // Use a dedicated service name to avoid collisions with other tooling. public static let service = "gitw.github.com" + internal static func friendly(status: OSStatus, op: String) -> String { + switch status { + case errSecDuplicateItem: + return "\(op) failed: Keychain item already exists." + case errSecInteractionNotAllowed: + return "\(op) failed: Keychain interaction not allowed (-25308). Possibly using the wrong user/session. Run as the logged-in GUI user and ensure the login keychain is unlocked." + default: + return "\(op) failed: \(status)" + } + } + /// Load credentials for a given alias. /// /// - alias: Local selector key. Not necessarily the GitHub username. @@ -51,7 +62,7 @@ public enum KeychainStore { let status = SecItemCopyMatching(query as CFDictionary, &item) if status == errSecItemNotFound { return nil } guard status == errSecSuccess else { - throw GitwError.keychain("SecItemCopyMatching failed: \(status)") + throw GitwError.keychain(friendly(status: status, op: "SecItemCopyMatching")) } guard let dict = item as? [String: Any], @@ -70,7 +81,7 @@ public enum KeychainStore { } /// Save credentials under a local alias. - public static func save(alias: String, profile: GitwProfile) throws { + public static func save(alias: String, profile: GitwProfile, overwrite: Bool) throws { let json = try JSONEncoder().encode(profile) let attrs: [String: Any] = [ @@ -85,6 +96,9 @@ public enum KeychainStore { let status = SecItemAdd(attrs as CFDictionary, nil) if status == errSecDuplicateItem { + if !overwrite { + throw GitwError.keychain("Keychain item already exists for alias \(alias).") + } let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, @@ -95,12 +109,12 @@ public enum KeychainStore { ] let s2 = SecItemUpdate(query as CFDictionary, update as CFDictionary) guard s2 == errSecSuccess else { - throw GitwError.keychain("SecItemUpdate failed: \(s2)") + throw GitwError.keychain(friendly(status: s2, op: "SecItemUpdate")) } return } guard status == errSecSuccess else { - throw GitwError.keychain("SecItemAdd failed: \(status)") + throw GitwError.keychain(friendly(status: status, op: "SecItemAdd")) } } @@ -113,7 +127,7 @@ public enum KeychainStore { let status = SecItemDelete(query as CFDictionary) if status == errSecItemNotFound { return } guard status == errSecSuccess else { - throw GitwError.keychain("SecItemDelete failed: \(status)") + throw GitwError.keychain(friendly(status: status, op: "SecItemDelete")) } } } diff --git a/Tests/GitwCoreTests/GitwAppTests.swift b/Tests/GitwCoreTests/GitwAppTests.swift index 4d29671..e406b7f 100644 --- a/Tests/GitwCoreTests/GitwAppTests.swift +++ b/Tests/GitwCoreTests/GitwAppTests.swift @@ -13,7 +13,10 @@ final class MockKeychain: KeychainProviding { return profileByAlias[alias] } - func save(alias: String, profile: GitwProfile) throws { + func save(alias: String, profile: GitwProfile, overwrite: Bool) throws { + if overwrite == false, profileByAlias[alias] != nil { + throw GitwError.keychain("Keychain item already exists for alias \(alias).") + } saved.append((alias, profile)) profileByAlias[alias] = profile } From 8bc32aee0facb37cd11e87fbc922ed7889ae94fb Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 17 Mar 2026 12:16:58 +0000 Subject: [PATCH 7/7] 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) } }