From 647005f468d5a62097bb78b7613d6307e147457b Mon Sep 17 00:00:00 2001 From: racgoo Date: Tue, 3 Mar 2026 23:47:41 +0900 Subject: [PATCH 1/4] feat: add Cred with factory methods and CredType enum --- index.d.ts | 276 +++++++++++++++++++++++++++++++++++++++ index.js | 106 +++++++-------- src/cred.rs | 363 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 694 insertions(+), 52 deletions(-) create mode 100644 src/cred.rs diff --git a/index.d.ts b/index.d.ts index cb86dfe..db7a93d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1069,6 +1069,263 @@ export declare class ConfigEntries extends Iterator { next(value?: void): IteratorResult } +/** + * A structure to represent git credentials in libgit2. + * + * Create instances via the static factory methods: `default()`, `sshKeyFromAgent()`, + * `sshKey()`, `sshKeyFromMemory()`, `userpassPlaintext()`, `username()`, `credentialHelper()`. + */ +export declare class Cred { + /** + * Create a "default" credential usable for Negotiate mechanisms like NTLM or Kerberos authentication. + * + * @category Cred + * + * @signature + * ```ts + * class Cred { + * static default(): Cred; + * } + * ``` + * + * @returns {Cred} A new Cred instance. + * @throws Throws error if the credential cannot be created. + * + * @example + * + * ```ts + * import { Cred } from 'es-git'; + * + * const cred = Cred.default(); + * ``` + */ + static default(): Cred + /** + * Create a new SSH key credential object used for querying an ssh-agent. + * + * The username specified is the username to authenticate. + * + * @category Cred + * + * @signature + * ```ts + * class Cred { + * static sshKeyFromAgent(username: string): Cred; + * } + * ``` + * + * @param {string} username - The username to authenticate. + * @returns {Cred} A new Cred instance. + * @throws Throws error if the ssh-agent is not available or the credential cannot be created. + * + * @example + * + * ```ts + * import { Cred } from 'es-git'; + * + * const cred = Cred.sshKeyFromAgent('git'); + * ``` + */ + static sshKeyFromAgent(username: string): Cred + /** + * Create a new passphrase-protected SSH key credential object from file paths. + * + * @category Cred + * + * @signature + * ```ts + * class Cred { + * static sshKey( + * username: string, + * publicKeyPath: string | null | undefined, + * privateKeyPath: string, + * passphrase?: string | null | undefined, + * ): Cred; + * } + * ``` + * + * @param {string} username - The username to authenticate. + * @param {string | null | undefined} publicKeyPath - Path to the public key file. If `null` or `undefined`, the public key is derived from the private key. + * @param {string} privateKeyPath - Path to the private key file. + * @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. + * @returns {Cred} A new Cred instance. + * @throws Throws error if the key files cannot be read or the credential cannot be created. + * + * @example + * + * ```ts + * import { Cred } from 'es-git'; + * + * const cred = Cred.sshKey('git', null, '/home/user/.ssh/id_ed25519', null); + * ``` + */ + static sshKey(username: string, publicKeyPath: string | undefined | null, privateKeyPath: string, passphrase?: string | undefined | null): Cred + /** + * Create a new SSH key credential object reading the keys from memory. + * + * @category Cred + * + * @signature + * ```ts + * class Cred { + * static sshKeyFromMemory( + * username: string, + * publicKey: string | null | undefined, + * privateKey: string, + * passphrase?: string | null | undefined, + * ): Cred; + * } + * ``` + * + * @param {string} username - The username to authenticate. + * @param {string | null | undefined} publicKey - The public key content. If `null` or `undefined`, the public key is derived from the private key. + * @param {string} privateKey - The private key content. + * @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. + * @returns {Cred} A new Cred instance. + * @throws Throws error if the key content is invalid or the credential cannot be created. + * + * @example + * + * ```ts + * import { Cred } from 'es-git'; + * + * const privateKey = await fs.readFile('/home/user/.ssh/id_ed25519', 'utf-8'); + * const cred = Cred.sshKeyFromMemory('git', null, privateKey, null); + * ``` + */ + static sshKeyFromMemory(username: string, publicKey: string | undefined | null, privateKey: string, passphrase?: string | undefined | null): Cred + /** + * Create a new plain-text username and password credential object. + * + * @category Cred + * + * @signature + * ```ts + * class Cred { + * static userpassPlaintext(username: string, password: string): Cred; + * } + * ``` + * + * @param {string} username - The username to authenticate. + * @param {string} password - The password to authenticate. + * @returns {Cred} A new Cred instance. + * @throws Throws error if the credential cannot be created. + * + * @example + * + * ```ts + * import { Cred } from 'es-git'; + * + * const cred = Cred.userpassPlaintext('user', 'password'); + * ``` + */ + static userpassPlaintext(username: string, password: string): Cred + /** + * Attempt to read `credential.helper` according to gitcredentials(7). + * + * This function will attempt to parse the user's `credential.helper` configuration, + * invoke the necessary processes, and read off what the username/password should be + * for a particular URL. The returned credential will be a username/password credential + * if successful. + * + * @category Cred + * + * @signature + * ```ts + * class Cred { + * static credentialHelper( + * config: Config, + * url: string, + * username?: string | null | undefined, + * ): Cred; + * } + * ``` + * + * @param {Config} config - Git configuration to read `credential.helper` from. + * @param {string} url - The URL to get credentials for. + * @param {string | null | undefined} [username] - Optional username hint. + * @returns {Cred} A new Cred instance containing the username/password from the helper. + * @throws Throws error if the helper fails or no credential is found. + * + * @example + * + * ```ts + * import { openRepository, Cred } from 'es-git'; + * + * const repo = await openRepository('.'); + * const config = repo.config(); + * const cred = Cred.credentialHelper(config, 'https://github.com/user/repo'); + * ``` + */ + static credentialHelper(config: Config, url: string, username?: string | undefined | null): Cred + /** + * Create a credential to specify a username. + * + * This is used with SSH authentication to query for the username if none is specified in the URL. + * + * @category Cred + * + * @signature + * ```ts + * class Cred { + * static username(username: string): Cred; + * } + * ``` + * + * @param {string} username - The username to authenticate. + * @returns {Cred} A new Cred instance. + * @throws Throws error if the credential cannot be created. + * + * @example + * + * ```ts + * import { Cred } from 'es-git'; + * + * const cred = Cred.username('git'); + * ``` + */ + static username(username: string): Cred + /** + * Check whether a credential object contains username information. + * + * @category Cred/Methods + * + * @signature + * ```ts + * class Cred { + * hasUsername(): boolean; + * } + * ``` + * + * @returns {boolean} Returns `true` if this credential contains username information. + */ + hasUsername(): boolean + /** + * Return the type of credentials that this object represents. + * + * @category Cred/Methods + * + * @signature + * ```ts + * class Cred { + * credtype(): CredType; + * } + * ``` + * + * @returns {CredType} The type of this credential. + * + * @example + * + * ```ts + * import { Cred } from 'es-git'; + * + * const cred = Cred.userpassPlaintext('user', 'password'); + * console.log(cred.credtype()); // 'UserpassPlaintext' + * ``` + */ + credtype(): CredType +} + /** * An iterator over the diffs in a delta. * @@ -7045,6 +7302,25 @@ export type CredentialType = 'Default'| 'SSHKey'| 'Plain'; +/** + * - `UserpassPlaintext` : Username and password in plain text. + * - `SshKey` : SSH key (from file). + * - `SshCustom` : SSH key with custom signature. + * - `Default` : Default (e.g. NTLM, Kerberos). + * - `SshInteractive` : SSH interactive. + * - `Username` : Username only. + * - `SshMemory` : SSH key from memory. + */ +export declare enum CredType { + UserpassPlaintext = 0, + SshKey = 1, + SshCustom = 2, + Default = 3, + SshInteractive = 4, + Username = 5, + SshMemory = 6 +} + export interface DeleteNoteOptions { /** * Signature of the notes commit author. diff --git a/index.js b/index.js index 61e2712..c3b0858 100644 --- a/index.js +++ b/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('es-git-android-arm64') const bindingPackageVersion = require('es-git-android-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('es-git-android-arm-eabi') const bindingPackageVersion = require('es-git-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('es-git-win32-x64-gnu') const bindingPackageVersion = require('es-git-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('es-git-win32-x64-msvc') const bindingPackageVersion = require('es-git-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('es-git-win32-ia32-msvc') const bindingPackageVersion = require('es-git-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('es-git-win32-arm64-msvc') const bindingPackageVersion = require('es-git-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('es-git-darwin-universal') const bindingPackageVersion = require('es-git-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('es-git-darwin-x64') const bindingPackageVersion = require('es-git-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('es-git-darwin-arm64') const bindingPackageVersion = require('es-git-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('es-git-freebsd-x64') const bindingPackageVersion = require('es-git-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('es-git-freebsd-arm64') const bindingPackageVersion = require('es-git-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('es-git-linux-x64-musl') const bindingPackageVersion = require('es-git-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('es-git-linux-x64-gnu') const bindingPackageVersion = require('es-git-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm64-musl') const bindingPackageVersion = require('es-git-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm64-gnu') const bindingPackageVersion = require('es-git-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm-musleabihf') const bindingPackageVersion = require('es-git-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('es-git-linux-arm-gnueabihf') const bindingPackageVersion = require('es-git-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('es-git-linux-loong64-musl') const bindingPackageVersion = require('es-git-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('es-git-linux-loong64-gnu') const bindingPackageVersion = require('es-git-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('es-git-linux-riscv64-musl') const bindingPackageVersion = require('es-git-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('es-git-linux-riscv64-gnu') const bindingPackageVersion = require('es-git-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('es-git-linux-ppc64-gnu') const bindingPackageVersion = require('es-git-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('es-git-linux-s390x-gnu') const bindingPackageVersion = require('es-git-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('es-git-openharmony-arm64') const bindingPackageVersion = require('es-git-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('es-git-openharmony-x64') const bindingPackageVersion = require('es-git-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('es-git-openharmony-arm') const bindingPackageVersion = require('es-git-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -582,6 +582,7 @@ module.exports.Branches = nativeBinding.Branches module.exports.Commit = nativeBinding.Commit module.exports.Config = nativeBinding.Config module.exports.ConfigEntries = nativeBinding.ConfigEntries +module.exports.Cred = nativeBinding.Cred module.exports.Deltas = nativeBinding.Deltas module.exports.Describe = nativeBinding.Describe module.exports.Diff = nativeBinding.Diff @@ -621,6 +622,7 @@ module.exports.ConfigLevel = nativeBinding.ConfigLevel module.exports.createMailmapFromBuffer = nativeBinding.createMailmapFromBuffer module.exports.createSignature = nativeBinding.createSignature module.exports.CredentialType = nativeBinding.CredentialType +module.exports.CredType = nativeBinding.CredType module.exports.DeltaType = nativeBinding.DeltaType module.exports.DiffFlags = nativeBinding.DiffFlags module.exports.diffFlagsContains = nativeBinding.diffFlagsContains diff --git a/src/cred.rs b/src/cred.rs new file mode 100644 index 0000000..14cec5e --- /dev/null +++ b/src/cred.rs @@ -0,0 +1,363 @@ +use crate::config::Config; +use napi_derive::napi; +use std::path::Path; + +#[napi] +#[repr(u8)] +#[derive(Copy, Clone)] +/// - `UserpassPlaintext` : Username and password in plain text. +/// - `SshKey` : SSH key (from file). +/// - `SshCustom` : SSH key with custom signature. +/// - `Default` : Default (e.g. NTLM, Kerberos). +/// - `SshInteractive` : SSH interactive. +/// - `Username` : Username only. +/// - `SshMemory` : SSH key from memory. +pub enum CredType { + UserpassPlaintext, + SshKey, + SshCustom, + Default, + SshInteractive, + Username, + SshMemory, +} + +impl From for CredType { + fn from(bits: u32) -> Self { + let cred_type = git2::CredentialType::from_bits_truncate(bits); + if cred_type.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { + return Self::UserpassPlaintext; + } + if cred_type.contains(git2::CredentialType::SSH_KEY) { + return Self::SshKey; + } + if cred_type.contains(git2::CredentialType::SSH_CUSTOM) { + return Self::SshCustom; + } + if cred_type.contains(git2::CredentialType::DEFAULT) { + return Self::Default; + } + if cred_type.contains(git2::CredentialType::SSH_INTERACTIVE) { + return Self::SshInteractive; + } + if cred_type.contains(git2::CredentialType::USERNAME) { + return Self::Username; + } + if cred_type.contains(git2::CredentialType::SSH_MEMORY) { + return Self::SshMemory; + } + Self::Default + } +} + +#[napi] +/// A structure to represent git credentials in libgit2. +/// +/// Create instances via the static factory methods: `default()`, `sshKeyFromAgent()`, +/// `sshKey()`, `sshKeyFromMemory()`, `userpassPlaintext()`, `username()`, `credentialHelper()`. +pub struct Cred { + pub(crate) inner: git2::Cred, +} + +impl From for Cred { + fn from(inner: git2::Cred) -> Self { + Cred { inner } + } +} + +impl From for git2::Cred { + fn from(cred: Cred) -> Self { + cred.inner + } +} + +#[napi] +impl Cred { + #[napi(js_name = "default")] + /// Create a "default" credential usable for Negotiate mechanisms like NTLM or Kerberos authentication. + /// + /// @category Cred + /// + /// @signature + /// ```ts + /// class Cred { + /// static default(): Cred; + /// } + /// ``` + /// + /// @returns {Cred} A new Cred instance. + /// @throws Throws error if the credential cannot be created. + /// + /// @example + /// + /// ```ts + /// import { Cred } from 'es-git'; + /// + /// const cred = Cred.default(); + /// ``` + pub fn create_default() -> crate::Result { + let inner = git2::Cred::default().map_err(crate::Error::from)?; + Ok(Cred { inner }) + } + + #[napi] + /// Create a new SSH key credential object used for querying an ssh-agent. + /// + /// The username specified is the username to authenticate. + /// + /// @category Cred + /// + /// @signature + /// ```ts + /// class Cred { + /// static sshKeyFromAgent(username: string): Cred; + /// } + /// ``` + /// + /// @param {string} username - The username to authenticate. + /// @returns {Cred} A new Cred instance. + /// @throws Throws error if the ssh-agent is not available or the credential cannot be created. + /// + /// @example + /// + /// ```ts + /// import { Cred } from 'es-git'; + /// + /// const cred = Cred.sshKeyFromAgent('git'); + /// ``` + pub fn ssh_key_from_agent(username: String) -> crate::Result { + let inner = git2::Cred::ssh_key_from_agent(&username)?; + Ok(Cred { inner }) + } + + #[napi] + /// Create a new passphrase-protected SSH key credential object from file paths. + /// + /// @category Cred + /// + /// @signature + /// ```ts + /// class Cred { + /// static sshKey( + /// username: string, + /// publicKeyPath: string | null | undefined, + /// privateKeyPath: string, + /// passphrase?: string | null | undefined, + /// ): Cred; + /// } + /// ``` + /// + /// @param {string} username - The username to authenticate. + /// @param {string | null | undefined} publicKeyPath - Path to the public key file. If `null` or `undefined`, the public key is derived from the private key. + /// @param {string} privateKeyPath - Path to the private key file. + /// @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. + /// @returns {Cred} A new Cred instance. + /// @throws Throws error if the key files cannot be read or the credential cannot be created. + /// + /// @example + /// + /// ```ts + /// import { Cred } from 'es-git'; + /// + /// const cred = Cred.sshKey('git', null, '/home/user/.ssh/id_ed25519', null); + /// ``` + pub fn ssh_key( + username: String, + public_key_path: Option, + private_key_path: String, + passphrase: Option, + ) -> crate::Result { + let inner = git2::Cred::ssh_key( + &username, + public_key_path.as_deref().map(Path::new), + Path::new(&private_key_path), + passphrase.as_deref(), + )?; + Ok(Cred { inner }) + } + + #[napi] + /// Create a new SSH key credential object reading the keys from memory. + /// + /// @category Cred + /// + /// @signature + /// ```ts + /// class Cred { + /// static sshKeyFromMemory( + /// username: string, + /// publicKey: string | null | undefined, + /// privateKey: string, + /// passphrase?: string | null | undefined, + /// ): Cred; + /// } + /// ``` + /// + /// @param {string} username - The username to authenticate. + /// @param {string | null | undefined} publicKey - The public key content. If `null` or `undefined`, the public key is derived from the private key. + /// @param {string} privateKey - The private key content. + /// @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. + /// @returns {Cred} A new Cred instance. + /// @throws Throws error if the key content is invalid or the credential cannot be created. + /// + /// @example + /// + /// ```ts + /// import { Cred } from 'es-git'; + /// + /// const privateKey = await fs.readFile('/home/user/.ssh/id_ed25519', 'utf-8'); + /// const cred = Cred.sshKeyFromMemory('git', null, privateKey, null); + /// ``` + pub fn ssh_key_from_memory( + username: String, + public_key: Option, + private_key: String, + passphrase: Option, + ) -> crate::Result { + let inner = git2::Cred::ssh_key_from_memory(&username, public_key.as_deref(), &private_key, passphrase.as_deref())?; + Ok(Cred { inner }) + } + + #[napi] + /// Create a new plain-text username and password credential object. + /// + /// @category Cred + /// + /// @signature + /// ```ts + /// class Cred { + /// static userpassPlaintext(username: string, password: string): Cred; + /// } + /// ``` + /// + /// @param {string} username - The username to authenticate. + /// @param {string} password - The password to authenticate. + /// @returns {Cred} A new Cred instance. + /// @throws Throws error if the credential cannot be created. + /// + /// @example + /// + /// ```ts + /// import { Cred } from 'es-git'; + /// + /// const cred = Cred.userpassPlaintext('user', 'password'); + /// ``` + pub fn userpass_plaintext(username: String, password: String) -> crate::Result { + let inner = git2::Cred::userpass_plaintext(&username, &password)?; + Ok(Cred { inner }) + } + + #[napi] + /// Attempt to read `credential.helper` according to gitcredentials(7). + /// + /// This function will attempt to parse the user's `credential.helper` configuration, + /// invoke the necessary processes, and read off what the username/password should be + /// for a particular URL. The returned credential will be a username/password credential + /// if successful. + /// + /// @category Cred + /// + /// @signature + /// ```ts + /// class Cred { + /// static credentialHelper( + /// config: Config, + /// url: string, + /// username?: string | null | undefined, + /// ): Cred; + /// } + /// ``` + /// + /// @param {Config} config - Git configuration to read `credential.helper` from. + /// @param {string} url - The URL to get credentials for. + /// @param {string | null | undefined} [username] - Optional username hint. + /// @returns {Cred} A new Cred instance containing the username/password from the helper. + /// @throws Throws error if the helper fails or no credential is found. + /// + /// @example + /// + /// ```ts + /// import { openRepository, Cred } from 'es-git'; + /// + /// const repo = await openRepository('.'); + /// const config = repo.config(); + /// const cred = Cred.credentialHelper(config, 'https://github.com/user/repo'); + /// ``` + pub fn credential_helper(config: &Config, url: String, username: Option) -> crate::Result { + let inner = git2::Cred::credential_helper(&config.inner, &url, username.as_deref())?; + Ok(Cred { inner }) + } + + #[napi] + /// Create a credential to specify a username. + /// + /// This is used with SSH authentication to query for the username if none is specified in the URL. + /// + /// @category Cred + /// + /// @signature + /// ```ts + /// class Cred { + /// static username(username: string): Cred; + /// } + /// ``` + /// + /// @param {string} username - The username to authenticate. + /// @returns {Cred} A new Cred instance. + /// @throws Throws error if the credential cannot be created. + /// + /// @example + /// + /// ```ts + /// import { Cred } from 'es-git'; + /// + /// const cred = Cred.username('git'); + /// ``` + pub fn username(username: String) -> crate::Result { + let inner = git2::Cred::username(&username)?; + Ok(Cred { inner }) + } + + #[napi] + /// Check whether a credential object contains username information. + /// + /// @category Cred/Methods + /// + /// @signature + /// ```ts + /// class Cred { + /// hasUsername(): boolean; + /// } + /// ``` + /// + /// @returns {boolean} Returns `true` if this credential contains username information. + pub fn has_username(&self) -> bool { + self.inner.has_username() + } + + #[napi] + /// Return the type of credentials that this object represents. + /// + /// @category Cred/Methods + /// + /// @signature + /// ```ts + /// class Cred { + /// credtype(): CredType; + /// } + /// ``` + /// + /// @returns {CredType} The type of this credential. + /// + /// @example + /// + /// ```ts + /// import { Cred } from 'es-git'; + /// + /// const cred = Cred.userpassPlaintext('user', 'password'); + /// console.log(cred.credtype()); // 'UserpassPlaintext' + /// ``` + pub fn credtype(&self) -> CredType { + self.inner.credtype().into() + } +} diff --git a/src/lib.rs b/src/lib.rs index 6540597..07600a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod checkout; pub mod cherrypick; pub mod commit; pub mod config; +pub mod cred; pub mod describe; pub mod diff; mod error; From b564d77bf4da5a11e1e2761dc1f3c05b3d206452 Mon Sep 17 00:00:00 2001 From: racgoo Date: Tue, 3 Mar 2026 23:52:20 +0900 Subject: [PATCH 2/4] test: add Cred tests --- tests/cred.spec.ts | 221 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 tests/cred.spec.ts diff --git a/tests/cred.spec.ts b/tests/cred.spec.ts new file mode 100644 index 0000000..4adeaef --- /dev/null +++ b/tests/cred.spec.ts @@ -0,0 +1,221 @@ +import { describe, expect, it } from 'vitest'; +import { Cred, CredType, openRepository } from '../index'; +import { useFixture } from './fixtures'; + +describe('cred', () => { + describe('default()', () => { + it('creates credential', () => { + const cred = Cred.default(); + expect(cred).toBeDefined(); + }); + + it('credtype is Default', () => { + const cred = Cred.default(); + expect(cred.credtype()).toBe(CredType.Default); + }); + + it('hasUsername returns false', () => { + const cred = Cred.default(); + expect(cred.hasUsername()).toBe(false); + }); + }); + + describe('userpassPlaintext()', () => { + it('creates credential', () => { + const cred = Cred.userpassPlaintext('user', 'password'); + expect(cred).toBeDefined(); + }); + + it('credtype is UserpassPlaintext', () => { + const cred = Cred.userpassPlaintext('user', 'password'); + expect(cred.credtype()).toBe(CredType.UserpassPlaintext); + }); + + it('hasUsername returns true', () => { + const cred = Cred.userpassPlaintext('user', 'password'); + expect(cred.hasUsername()).toBe(true); + }); + + it('accepts empty string password', () => { + const cred = Cred.userpassPlaintext('user', ''); + expect(cred.credtype()).toBe(CredType.UserpassPlaintext); + }); + }); + + describe('username()', () => { + it('creates credential', () => { + const cred = Cred.username('git'); + expect(cred).toBeDefined(); + }); + + it('credtype is Username', () => { + const cred = Cred.username('git'); + expect(cred.credtype()).toBe(CredType.Username); + }); + + it('hasUsername returns true', () => { + const cred = Cred.username('git'); + expect(cred.hasUsername()).toBe(true); + }); + + it('accepts arbitrary username', () => { + const cred = Cred.username('my-github-user'); + expect(cred.credtype()).toBe(CredType.Username); + expect(cred.hasUsername()).toBe(true); + }); + }); + + describe('sshKeyFromAgent()', () => { + it('creates credential or throws when no agent', () => { + try { + const cred = Cred.sshKeyFromAgent('git'); + expect(cred.credtype()).toBeDefined(); + expect(cred.hasUsername()).toBe(true); + } catch (e) { + expect(e).toBeDefined(); + } + }); + }); + + describe('sshKey()', () => { + // libgit2 defers key file I/O until the credential is actually used, + // so path-based tests can use nonexistent paths to verify the binding. + it('creates credential without publicKeyPath (derived from private key)', () => { + const cred = Cred.sshKey('git', null, '/path/to/id_ed25519', null); + expect(cred).toBeDefined(); + expect(cred.credtype()).toBe(CredType.SshKey); + expect(cred.hasUsername()).toBe(true); + }); + + it('creates credential with publicKeyPath provided', () => { + const cred = Cred.sshKey('git', '/path/to/id_ed25519.pub', '/path/to/id_ed25519', null); + expect(cred.credtype()).toBe(CredType.SshKey); + expect(cred.hasUsername()).toBe(true); + }); + + it('creates credential with both publicKeyPath and passphrase', () => { + const cred = Cred.sshKey('git', '/path/to/id_ed25519.pub', '/path/to/id_ed25519', 'my-passphrase'); + expect(cred.credtype()).toBe(CredType.SshKey); + }); + + it('creates credential without publicKeyPath but with passphrase', () => { + const cred = Cred.sshKey('git', null, '/path/to/id_rsa', 'my-passphrase'); + expect(cred.credtype()).toBe(CredType.SshKey); + }); + }); + + describe('sshKeyFromMemory()', () => { + // Minimal truncated PEM blocks — libgit2 may reject the content but the binding must + // accept the string parameters without panicking or type errors. + const DUMMY_PRIVATE_KEY = [ + '-----BEGIN OPENSSH PRIVATE KEY-----', + 'b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW', + 'QyNTUxOQAAACD3', + '-----END OPENSSH PRIVATE KEY-----', + ].join('\n'); + + const DUMMY_PUBLIC_KEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDummyPublicKeyForTestingPurposesOnly user@host'; + + it('creates credential without publicKey (null)', () => { + try { + const cred = Cred.sshKeyFromMemory('git', null, DUMMY_PRIVATE_KEY, null); + expect(cred.credtype()).toBe(CredType.SshMemory); + expect(cred.hasUsername()).toBe(true); + } catch (e) { + // libgit2 may reject truncated key content + expect(e).toBeDefined(); + } + }); + + it('creates credential with publicKey provided', () => { + try { + const cred = Cred.sshKeyFromMemory('git', DUMMY_PUBLIC_KEY, DUMMY_PRIVATE_KEY, null); + expect(cred.credtype()).toBe(CredType.SshMemory); + expect(cred.hasUsername()).toBe(true); + } catch (e) { + expect(e).toBeDefined(); + } + }); + + it('creates credential with publicKey and passphrase', () => { + try { + const cred = Cred.sshKeyFromMemory('git', DUMMY_PUBLIC_KEY, DUMMY_PRIVATE_KEY, 'passphrase'); + expect(cred.credtype()).toBe(CredType.SshMemory); + } catch (e) { + expect(e).toBeDefined(); + } + }); + + it('creates credential without publicKey but with passphrase', () => { + try { + const cred = Cred.sshKeyFromMemory('git', null, DUMMY_PRIVATE_KEY, 'passphrase'); + expect(cred.credtype()).toBe(CredType.SshMemory); + } catch (e) { + expect(e).toBeDefined(); + } + }); + }); + + describe('credentialHelper()', () => { + it('returns Cred or throws when no helper is configured', async () => { + const p = await useFixture('empty'); + const repo = await openRepository(p); + const config = repo.config(); + try { + const cred = Cred.credentialHelper(config, 'https://example.invalid/repo', null); + expect(cred).toBeDefined(); + expect(cred.credtype()).toBeDefined(); + } catch (e) { + expect(e).toBeDefined(); + } + }); + + it('accepts username hint', async () => { + const p = await useFixture('empty'); + const repo = await openRepository(p); + const config = repo.config(); + try { + const cred = Cred.credentialHelper(config, 'https://example.invalid/repo', 'user'); + expect(cred).toBeDefined(); + } catch (e) { + expect(e).toBeDefined(); + } + }); + }); + + describe('hasUsername()', () => { + it('returns false for Default credential', () => { + expect(Cred.default().hasUsername()).toBe(false); + }); + + it('returns true for userpassPlaintext credential', () => { + expect(Cred.userpassPlaintext('user', 'pass').hasUsername()).toBe(true); + }); + + it('returns true for username credential', () => { + expect(Cred.username('git').hasUsername()).toBe(true); + }); + + it('returns true for sshKey credential', () => { + expect(Cred.sshKey('git', null, '/path/to/key', null).hasUsername()).toBe(true); + }); + }); + + describe('credtype()', () => { + it('returns Default for default()', () => { + expect(Cred.default().credtype()).toBe(CredType.Default); + }); + + it('returns UserpassPlaintext for userpassPlaintext()', () => { + expect(Cred.userpassPlaintext('user', 'pass').credtype()).toBe(CredType.UserpassPlaintext); + }); + + it('returns Username for username()', () => { + expect(Cred.username('git').credtype()).toBe(CredType.Username); + }); + + it('returns SshKey for sshKey()', () => { + expect(Cred.sshKey('git', null, '/path/to/key', null).credtype()).toBe(CredType.SshKey); + }); + }); +}); From 2dd6952c4c7ae7e499142e9cb9a2afadb2e6b1fa Mon Sep 17 00:00:00 2001 From: racgoo Date: Wed, 4 Mar 2026 00:37:03 +0900 Subject: [PATCH 3/4] fix: use Cred internally for remote.rs credential construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TODO: Credential struct is a flat DTO mixing all credential fields regardless of type. Consider replacing it with a builder pattern where callers construct Cred directly via factory methods (Cred.default(), Cred.sshKeyFromAgent(), etc.) and inject it into fetch/push options — eliminating the Credential struct entirely. --- src/remote.rs | 55 ++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/remote.rs b/src/remote.rs index 5adecd2..495422b 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,7 +1,7 @@ +use crate::cred::Cred; use crate::repository::Repository; use napi::bindgen_prelude::*; use napi_derive::napi; -use std::path::Path; use std::sync::RwLock; #[napi(string_enum)] @@ -76,32 +76,37 @@ pub struct Credential { pub password: Option, } -impl Credential { - pub(crate) fn to_git2_cred(&self) -> std::result::Result { - let fallback = "git".to_string(); - let cred = match self.r#type { - CredentialType::Default => git2::Cred::default(), - CredentialType::SSHKeyFromAgent => { - git2::Cred::ssh_key_from_agent(self.username.to_owned().unwrap_or(fallback).as_ref()) - } - CredentialType::SSHKeyFromPath => git2::Cred::ssh_key( - self.username.to_owned().unwrap_or(fallback).as_ref(), - self.public_key_path.as_ref().map(Path::new), - Path::new(&self.private_key_path.to_owned().unwrap()), - self.passphrase.as_ref().map(String::as_ref), - ), - CredentialType::SSHKey => git2::Cred::ssh_key_from_memory( - self.username.to_owned().unwrap_or(fallback).as_ref(), - self.public_key.as_ref().map(String::as_ref), - &self.private_key.to_owned().unwrap(), - self.passphrase.as_ref().map(String::as_ref), +impl TryFrom<&Credential> for Cred { + type Error = crate::Error; + + fn try_from(cred: &Credential) -> crate::Result { + let username = cred.username.clone().unwrap_or_else(|| "git".to_string()); + match &cred.r#type { + CredentialType::Default => Cred::create_default(), + CredentialType::SSHKeyFromAgent => Cred::ssh_key_from_agent(username), + CredentialType::SSHKeyFromPath => Cred::ssh_key( + username, + cred.public_key_path.clone(), + cred.private_key_path.clone().unwrap_or_default(), + cred.passphrase.clone(), ), - CredentialType::Plain => git2::Cred::userpass_plaintext( - self.username.to_owned().unwrap_or(fallback).as_ref(), - &self.password.to_owned().unwrap(), + CredentialType::SSHKey => Cred::ssh_key_from_memory( + username, + cred.public_key.clone(), + cred.private_key.clone().unwrap_or_default(), + cred.passphrase.clone(), ), - }?; - Ok(cred) + CredentialType::Plain => Cred::userpass_plaintext(username, cred.password.clone().unwrap_or_default()), + } + } +} + +impl Credential { + pub(crate) fn to_git2_cred(&self) -> std::result::Result { + Cred::try_from(self).map(Into::into).map_err(|e| match e { + crate::Error::Git2(g) => g, + other => git2::Error::from_str(&other.to_string()), + }) } } From b840a1736a59875cc9d4f3cf971b27ef83101d4a Mon Sep 17 00:00:00 2001 From: racgoo Date: Sat, 7 Mar 2026 17:15:07 +0900 Subject: [PATCH 4/4] fix: replace Credential DTO with recipe-based Cred in remote options --- index.d.ts | 56 +++----------- index.js | 1 - src/cred.rs | 178 ++++++++++++++++++++++++++++++++------------- src/remote.rs | 66 ++--------------- tests/cred.spec.ts | 66 +++++++++-------- 5 files changed, 178 insertions(+), 189 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1633de8..ee342f7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1089,7 +1089,6 @@ export declare class Cred { * ``` * * @returns {Cred} A new Cred instance. - * @throws Throws error if the credential cannot be created. * * @example * @@ -1116,7 +1115,6 @@ export declare class Cred { * * @param {string} username - The username to authenticate. * @returns {Cred} A new Cred instance. - * @throws Throws error if the ssh-agent is not available or the credential cannot be created. * * @example * @@ -1149,7 +1147,6 @@ export declare class Cred { * @param {string} privateKeyPath - Path to the private key file. * @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. * @returns {Cred} A new Cred instance. - * @throws Throws error if the key files cannot be read or the credential cannot be created. * * @example * @@ -1182,7 +1179,6 @@ export declare class Cred { * @param {string} privateKey - The private key content. * @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. * @returns {Cred} A new Cred instance. - * @throws Throws error if the key content is invalid or the credential cannot be created. * * @example * @@ -1209,7 +1205,6 @@ export declare class Cred { * @param {string} username - The username to authenticate. * @param {string} password - The password to authenticate. * @returns {Cred} A new Cred instance. - * @throws Throws error if the credential cannot be created. * * @example * @@ -1234,30 +1229,25 @@ export declare class Cred { * ```ts * class Cred { * static credentialHelper( - * config: Config, * url: string, * username?: string | null | undefined, * ): Cred; * } * ``` * - * @param {Config} config - Git configuration to read `credential.helper` from. * @param {string} url - The URL to get credentials for. * @param {string | null | undefined} [username] - Optional username hint. * @returns {Cred} A new Cred instance containing the username/password from the helper. - * @throws Throws error if the helper fails or no credential is found. * * @example * * ```ts - * import { openRepository, Cred } from 'es-git'; + * import { Cred } from 'es-git'; * - * const repo = await openRepository('.'); - * const config = repo.config(); - * const cred = Cred.credentialHelper(config, 'https://github.com/user/repo'); + * const cred = Cred.credentialHelper('https://github.com/user/repo'); * ``` */ - static credentialHelper(config: Config, url: string, username?: string | undefined | null): Cred + static credentialHelper(url: string, username?: string | undefined | null): Cred /** * Create a credential to specify a username. * @@ -1274,7 +1264,6 @@ export declare class Cred { * * @param {string} username - The username to authenticate. * @returns {Cred} A new Cred instance. - * @throws Throws error if the credential cannot be created. * * @example * @@ -1298,6 +1287,8 @@ export declare class Cred { * ``` * * @returns {boolean} Returns `true` if this credential contains username information. + * + * @throws Throws if the underlying resource is no longer available (e.g. key file deleted, credential helper config changed) since the credential was created. */ hasUsername(): boolean /** @@ -7453,36 +7444,6 @@ export interface CreateTagOptions { force?: boolean } -/** A interface to represent git credentials in libgit2. */ -export type Credential = { - type: 'Default'; -} | { - type: 'SSHKeyFromAgent'; - username?: string; -} | { - type: 'SSHKeyFromPath'; - username?: string; - publicKeyPath?: string; - privateKeyPath: string; - passphrase?: string; -} | { - type: 'SSHKey'; - username?: string; - publicKey?: string; - privateKey: string; - passphrase?: string; -} | { - type: 'Plain'; - username?: string; - password: string; -}; - -export type CredentialType = 'Default'| -'SSHKeyFromAgent'| -'SSHKeyFromPath'| -'SSHKey'| -'Plain'; - /** * - `UserpassPlaintext` : Username and password in plain text. * - `SshKey` : SSH key (from file). @@ -7911,7 +7872,7 @@ export interface ExtractedSignature { } export interface FetchOptions { - credential?: Credential + credential?: Cred /** Set the proxy options to use for the fetch operation. */ proxy?: ProxyOptions /** Set whether to perform a prune after the fetch. */ @@ -8699,12 +8660,13 @@ export interface ProxyOptions { } export interface PruneOptions { - credential?: Credential + credential?: Cred } /** Options to control the behavior of a git push. */ export interface PushOptions { - credential?: Credential + /** Set credential for the push operation. */ + credential?: Cred /** Set the proxy options to use for the push operation. */ proxy?: ProxyOptions /** diff --git a/index.js b/index.js index 31b6596..8246e40 100644 --- a/index.js +++ b/index.js @@ -622,7 +622,6 @@ module.exports.cloneRepository = nativeBinding.cloneRepository module.exports.ConfigLevel = nativeBinding.ConfigLevel module.exports.createMailmapFromBuffer = nativeBinding.createMailmapFromBuffer module.exports.createSignature = nativeBinding.createSignature -module.exports.CredentialType = nativeBinding.CredentialType module.exports.CredType = nativeBinding.CredType module.exports.DeltaType = nativeBinding.DeltaType module.exports.DiffFlags = nativeBinding.DiffFlags diff --git a/src/cred.rs b/src/cred.rs index 14cec5e..3975732 100644 --- a/src/cred.rs +++ b/src/cred.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use napi::bindgen_prelude::*; use napi_derive::napi; use std::path::Path; @@ -50,24 +50,93 @@ impl From for CredType { } } +/// Stores the parameters needed to construct a `git2::Cred` on demand. +/// Call `to_git2_cred()` to materialise the credential at the desired point in time. +#[derive(Clone)] +pub(crate) enum CredRecipe { + Default, + SshKeyFromAgent { + username: String, + }, + SshKey { + username: String, + public_key_path: Option, + private_key_path: String, + passphrase: Option, + }, + SshKeyFromMemory { + username: String, + public_key: Option, + private_key: String, + passphrase: Option, + }, + UserpassPlaintext { + username: String, + password: String, + }, + Username { + username: String, + }, + CredentialHelper { + url: String, + username: Option, + }, +} + #[napi] +#[derive(Clone)] /// A structure to represent git credentials in libgit2. /// /// Create instances via the static factory methods: `default()`, `sshKeyFromAgent()`, /// `sshKey()`, `sshKeyFromMemory()`, `userpassPlaintext()`, `username()`, `credentialHelper()`. pub struct Cred { - pub(crate) inner: git2::Cred, + pub(crate) recipe: CredRecipe, } -impl From for Cred { - fn from(inner: git2::Cred) -> Self { - Cred { inner } +// Required so that `Cred` can be used as a field inside `#[napi(object)]` structs (e.g. `FetchOptions`). +// napi-rs does not auto-generate `FromNapiValue` for `#[napi]` classes, so we implement it manually. +// Because `Cred: Clone`, we extract the raw pointer via `napi_unwrap` and clone the value. +impl FromNapiValue for Cred { + unsafe fn from_napi_value(env: napi::sys::napi_env, napi_val: napi::sys::napi_value) -> napi::Result { + let mut ptr = std::ptr::null_mut::(); + check_status!( + napi::sys::napi_unwrap(env, napi_val, &mut ptr), + "Failed to unwrap `Cred` from JS value" + )?; + Ok((*(ptr as *const Cred)).clone()) } } -impl From for git2::Cred { - fn from(cred: Cred) -> Self { - cred.inner +// Crate-internal methods: not exposed to JS. Converts the stored recipe into a live `git2::Cred`. +impl Cred { + pub(crate) fn to_git2_cred(&self) -> std::result::Result { + match &self.recipe { + CredRecipe::Default => git2::Cred::default(), + CredRecipe::SshKeyFromAgent { username } => git2::Cred::ssh_key_from_agent(username), + CredRecipe::SshKey { + username, + public_key_path, + private_key_path, + passphrase, + } => git2::Cred::ssh_key( + username, + public_key_path.as_deref().map(Path::new), + Path::new(private_key_path), + passphrase.as_deref(), + ), + CredRecipe::SshKeyFromMemory { + username, + public_key, + private_key, + passphrase, + } => git2::Cred::ssh_key_from_memory(username, public_key.as_deref(), private_key, passphrase.as_deref()), + CredRecipe::UserpassPlaintext { username, password } => git2::Cred::userpass_plaintext(username, password), + CredRecipe::Username { username } => git2::Cred::username(username), + CredRecipe::CredentialHelper { url, username } => { + let config = git2::Config::open_default()?; + git2::Cred::credential_helper(&config, url, username.as_deref()) + } + } } } @@ -86,7 +155,6 @@ impl Cred { /// ``` /// /// @returns {Cred} A new Cred instance. - /// @throws Throws error if the credential cannot be created. /// /// @example /// @@ -95,9 +163,10 @@ impl Cred { /// /// const cred = Cred.default(); /// ``` - pub fn create_default() -> crate::Result { - let inner = git2::Cred::default().map_err(crate::Error::from)?; - Ok(Cred { inner }) + pub fn create_default() -> Cred { + Cred { + recipe: CredRecipe::Default, + } } #[napi] @@ -116,7 +185,6 @@ impl Cred { /// /// @param {string} username - The username to authenticate. /// @returns {Cred} A new Cred instance. - /// @throws Throws error if the ssh-agent is not available or the credential cannot be created. /// /// @example /// @@ -125,9 +193,10 @@ impl Cred { /// /// const cred = Cred.sshKeyFromAgent('git'); /// ``` - pub fn ssh_key_from_agent(username: String) -> crate::Result { - let inner = git2::Cred::ssh_key_from_agent(&username)?; - Ok(Cred { inner }) + pub fn ssh_key_from_agent(username: String) -> Cred { + Cred { + recipe: CredRecipe::SshKeyFromAgent { username }, + } } #[napi] @@ -152,7 +221,6 @@ impl Cred { /// @param {string} privateKeyPath - Path to the private key file. /// @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. /// @returns {Cred} A new Cred instance. - /// @throws Throws error if the key files cannot be read or the credential cannot be created. /// /// @example /// @@ -166,14 +234,15 @@ impl Cred { public_key_path: Option, private_key_path: String, passphrase: Option, - ) -> crate::Result { - let inner = git2::Cred::ssh_key( - &username, - public_key_path.as_deref().map(Path::new), - Path::new(&private_key_path), - passphrase.as_deref(), - )?; - Ok(Cred { inner }) + ) -> Cred { + Cred { + recipe: CredRecipe::SshKey { + username, + public_key_path, + private_key_path, + passphrase, + }, + } } #[napi] @@ -198,7 +267,6 @@ impl Cred { /// @param {string} privateKey - The private key content. /// @param {string | null | undefined} [passphrase] - Passphrase for the private key, if any. /// @returns {Cred} A new Cred instance. - /// @throws Throws error if the key content is invalid or the credential cannot be created. /// /// @example /// @@ -213,9 +281,15 @@ impl Cred { public_key: Option, private_key: String, passphrase: Option, - ) -> crate::Result { - let inner = git2::Cred::ssh_key_from_memory(&username, public_key.as_deref(), &private_key, passphrase.as_deref())?; - Ok(Cred { inner }) + ) -> Cred { + Cred { + recipe: CredRecipe::SshKeyFromMemory { + username, + public_key, + private_key, + passphrase, + }, + } } #[napi] @@ -233,7 +307,6 @@ impl Cred { /// @param {string} username - The username to authenticate. /// @param {string} password - The password to authenticate. /// @returns {Cred} A new Cred instance. - /// @throws Throws error if the credential cannot be created. /// /// @example /// @@ -242,9 +315,10 @@ impl Cred { /// /// const cred = Cred.userpassPlaintext('user', 'password'); /// ``` - pub fn userpass_plaintext(username: String, password: String) -> crate::Result { - let inner = git2::Cred::userpass_plaintext(&username, &password)?; - Ok(Cred { inner }) + pub fn userpass_plaintext(username: String, password: String) -> Cred { + Cred { + recipe: CredRecipe::UserpassPlaintext { username, password }, + } } #[napi] @@ -261,31 +335,27 @@ impl Cred { /// ```ts /// class Cred { /// static credentialHelper( - /// config: Config, /// url: string, /// username?: string | null | undefined, /// ): Cred; /// } /// ``` /// - /// @param {Config} config - Git configuration to read `credential.helper` from. /// @param {string} url - The URL to get credentials for. /// @param {string | null | undefined} [username] - Optional username hint. /// @returns {Cred} A new Cred instance containing the username/password from the helper. - /// @throws Throws error if the helper fails or no credential is found. /// /// @example /// /// ```ts - /// import { openRepository, Cred } from 'es-git'; + /// import { Cred } from 'es-git'; /// - /// const repo = await openRepository('.'); - /// const config = repo.config(); - /// const cred = Cred.credentialHelper(config, 'https://github.com/user/repo'); + /// const cred = Cred.credentialHelper('https://github.com/user/repo'); /// ``` - pub fn credential_helper(config: &Config, url: String, username: Option) -> crate::Result { - let inner = git2::Cred::credential_helper(&config.inner, &url, username.as_deref())?; - Ok(Cred { inner }) + pub fn credential_helper(url: String, username: Option) -> Cred { + Cred { + recipe: CredRecipe::CredentialHelper { url, username }, + } } #[napi] @@ -304,7 +374,6 @@ impl Cred { /// /// @param {string} username - The username to authenticate. /// @returns {Cred} A new Cred instance. - /// @throws Throws error if the credential cannot be created. /// /// @example /// @@ -313,9 +382,10 @@ impl Cred { /// /// const cred = Cred.username('git'); /// ``` - pub fn username(username: String) -> crate::Result { - let inner = git2::Cred::username(&username)?; - Ok(Cred { inner }) + pub fn username(username: String) -> Cred { + Cred { + recipe: CredRecipe::Username { username }, + } } #[napi] @@ -331,8 +401,10 @@ impl Cred { /// ``` /// /// @returns {boolean} Returns `true` if this credential contains username information. - pub fn has_username(&self) -> bool { - self.inner.has_username() + /// + /// @throws Throws if the underlying resource is no longer available (e.g. key file deleted, credential helper config changed) since the credential was created. + pub fn has_username(&self) -> crate::Result { + Ok(self.to_git2_cred()?.has_username()) } #[napi] @@ -358,6 +430,12 @@ impl Cred { /// console.log(cred.credtype()); // 'UserpassPlaintext' /// ``` pub fn credtype(&self) -> CredType { - self.inner.credtype().into() + match &self.recipe { + CredRecipe::Default => CredType::Default, + CredRecipe::SshKeyFromAgent { .. } | CredRecipe::SshKey { .. } => CredType::SshKey, + CredRecipe::SshKeyFromMemory { .. } => CredType::SshMemory, + CredRecipe::UserpassPlaintext { .. } | CredRecipe::CredentialHelper { .. } => CredType::UserpassPlaintext, + CredRecipe::Username { .. } => CredType::Username, + } } } diff --git a/src/remote.rs b/src/remote.rs index 495422b..bb4ad32 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -52,64 +52,6 @@ impl<'a> TryFrom> for Refspec { } } -#[napi(string_enum)] -#[derive(Copy, Clone)] -pub enum CredentialType { - Default, - SSHKeyFromAgent, - SSHKeyFromPath, - SSHKey, - Plain, -} - -#[napi(object)] -#[derive(Clone)] -/// A interface to represent git credentials in libgit2. -pub struct Credential { - pub r#type: CredentialType, - pub username: Option, - pub public_key_path: Option, - pub public_key: Option, - pub private_key_path: Option, - pub private_key: Option, - pub passphrase: Option, - pub password: Option, -} - -impl TryFrom<&Credential> for Cred { - type Error = crate::Error; - - fn try_from(cred: &Credential) -> crate::Result { - let username = cred.username.clone().unwrap_or_else(|| "git".to_string()); - match &cred.r#type { - CredentialType::Default => Cred::create_default(), - CredentialType::SSHKeyFromAgent => Cred::ssh_key_from_agent(username), - CredentialType::SSHKeyFromPath => Cred::ssh_key( - username, - cred.public_key_path.clone(), - cred.private_key_path.clone().unwrap_or_default(), - cred.passphrase.clone(), - ), - CredentialType::SSHKey => Cred::ssh_key_from_memory( - username, - cred.public_key.clone(), - cred.private_key.clone().unwrap_or_default(), - cred.passphrase.clone(), - ), - CredentialType::Plain => Cred::userpass_plaintext(username, cred.password.clone().unwrap_or_default()), - } - } -} - -impl Credential { - pub(crate) fn to_git2_cred(&self) -> std::result::Result { - Cred::try_from(self).map(Into::into).map_err(|e| match e { - crate::Error::Git2(g) => g, - other => git2::Error::from_str(&other.to_string()), - }) - } -} - #[napi(object)] pub struct ProxyOptions { /// Try to auto-detect the proxy from the git configuration. @@ -203,7 +145,7 @@ impl From for git2::RemoteRedirect { #[napi(object)] pub struct FetchOptions { - pub credential: Option, + pub credential: Option, /// Set the proxy options to use for the fetch operation. pub proxy: Option, /// Set whether to perform a prune after the fetch. @@ -259,7 +201,8 @@ impl<'a> FetchOptions { #[napi(object)] /// Options to control the behavior of a git push. pub struct PushOptions { - pub credential: Option, + /// Set credential for the push operation. + pub credential: Option, /// Set the proxy options to use for the push operation. pub proxy: Option, /// If the transport being used to push to the remote requires the creation @@ -322,7 +265,7 @@ pub struct FetchRemoteOptions { #[napi(object)] pub struct PruneOptions { - pub credential: Option, + pub credential: Option, } pub struct FetchRemoteTask { @@ -418,6 +361,7 @@ impl Task for PruneRemoteTask { Some(PruneOptions { credential: Some(cred), .. }) => { + let cred = cred.clone(); let mut callbacks = git2::RemoteCallbacks::new(); callbacks.credentials(move |_url, _username, _cred| cred.to_git2_cred()); Some(callbacks) diff --git a/tests/cred.spec.ts b/tests/cred.spec.ts index 4adeaef..09c02d7 100644 --- a/tests/cred.spec.ts +++ b/tests/cred.spec.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { Cred, CredType, openRepository } from '../index'; -import { useFixture } from './fixtures'; +import { Cred, CredType } from '../index'; describe('cred', () => { describe('default()', () => { @@ -53,11 +52,6 @@ describe('cred', () => { expect(cred.credtype()).toBe(CredType.Username); }); - it('hasUsername returns true', () => { - const cred = Cred.username('git'); - expect(cred.hasUsername()).toBe(true); - }); - it('accepts arbitrary username', () => { const cred = Cred.username('my-github-user'); expect(cred.credtype()).toBe(CredType.Username); @@ -105,8 +99,8 @@ describe('cred', () => { }); describe('sshKeyFromMemory()', () => { - // Minimal truncated PEM blocks — libgit2 may reject the content but the binding must - // accept the string parameters without panicking or type errors. + // libgit2 defers key content validation until the credential is actually used during + // authentication, so even truncated or invalid PEM does not throw at creation time. const DUMMY_PRIVATE_KEY = [ '-----BEGIN OPENSSH PRIVATE KEY-----', 'b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW', @@ -157,29 +151,15 @@ describe('cred', () => { }); describe('credentialHelper()', () => { - it('returns Cred or throws when no helper is configured', async () => { - const p = await useFixture('empty'); - const repo = await openRepository(p); - const config = repo.config(); - try { - const cred = Cred.credentialHelper(config, 'https://example.invalid/repo', null); - expect(cred).toBeDefined(); - expect(cred.credtype()).toBeDefined(); - } catch (e) { - expect(e).toBeDefined(); - } + it('creates credential', () => { + const cred = Cred.credentialHelper('https://example.invalid/repo', null); + expect(cred).toBeDefined(); + expect(cred.credtype()).toBe(CredType.UserpassPlaintext); }); - it('accepts username hint', async () => { - const p = await useFixture('empty'); - const repo = await openRepository(p); - const config = repo.config(); - try { - const cred = Cred.credentialHelper(config, 'https://example.invalid/repo', 'user'); - expect(cred).toBeDefined(); - } catch (e) { - expect(e).toBeDefined(); - } + it('accepts username hint', () => { + const cred = Cred.credentialHelper('https://example.invalid/repo', 'user'); + expect(cred).toBeDefined(); }); }); @@ -199,6 +179,32 @@ describe('cred', () => { it('returns true for sshKey credential', () => { expect(Cred.sshKey('git', null, '/path/to/key', null).hasUsername()).toBe(true); }); + + it('factory succeeds but hasUsername() throws if ssh-agent is unavailable', () => { + // sshKeyFromAgent() itself never throws — it only stores the recipe. + // hasUsername() calls to_git2_cred() which queries the agent, and may throw. + const cred = Cred.sshKeyFromAgent('git'); + expect(cred).toBeDefined(); + try { + const result = cred.hasUsername(); + expect(result).toBe(true); + } catch (e) { + expect(e).toBeDefined(); + } + }); + + it('factory succeeds but hasUsername() throws if credential helper is not configured', () => { + // credentialHelper() itself never throws — it only stores url + username hint. + // hasUsername() calls to_git2_cred() which runs the helper, and may throw. + const cred = Cred.credentialHelper('https://example.invalid/repo', null); + expect(cred).toBeDefined(); + try { + const result = cred.hasUsername(); + expect(result).toBe(true); + } catch (e) { + expect(e).toBeDefined(); + } + }); }); describe('credtype()', () => {