(opts: CommitWithDeps)
| 77 | } |
| 78 | |
| 79 | export async function commitWith(opts: CommitWithDeps): Promise<CommitResult> { |
| 80 | const dir = opts.dir ?? "/"; |
| 81 | if (!opts.message || opts.message.trim().length === 0) { |
| 82 | throw new GitError("EMSG", "commit message is required"); |
| 83 | } |
| 84 | |
| 85 | // Read the local config identity once; it sits between env and |
| 86 | // the GitClient default in precedence. A missing key resolves |
| 87 | // to undefined, so the fallback chain skips it cleanly. |
| 88 | const configName = await readConfigString(opts, dir, "user.name"); |
| 89 | const configEmail = await readConfigString(opts, dir, "user.email"); |
| 90 | const configIdent = |
| 91 | configName && configEmail ? { name: configName, email: configEmail } : undefined; |
| 92 | |
| 93 | const author = resolveIdent( |
| 94 | opts.author, |
| 95 | opts.env?.GIT_AUTHOR_NAME, |
| 96 | opts.env?.GIT_AUTHOR_EMAIL, |
| 97 | configIdent, |
| 98 | opts.defaultIdentity, |
| 99 | ); |
| 100 | if (!author) throw new MissingIdentityError(); |
| 101 | const committer = |
| 102 | resolveIdent( |
| 103 | opts.committer, |
| 104 | opts.env?.GIT_COMMITTER_NAME ?? opts.env?.GIT_AUTHOR_NAME, |
| 105 | opts.env?.GIT_COMMITTER_EMAIL ?? opts.env?.GIT_AUTHOR_EMAIL, |
| 106 | configIdent, |
| 107 | opts.defaultIdentity, |
| 108 | ) ?? author; |
| 109 | |
| 110 | let oid: string; |
| 111 | try { |
| 112 | oid = await opts.git.commit({ |
| 113 | fs: opts.fs, |
| 114 | dir, |
| 115 | message: opts.message, |
| 116 | author, |
| 117 | committer, |
| 118 | amend: opts.amend, |
| 119 | cache: opts.cache, |
| 120 | }); |
| 121 | } catch (cause) { |
| 122 | if (isNotARepositoryCause(cause)) throw new NotARepositoryError(dir, { cause }); |
| 123 | throw new GitError("ECOMMITFAIL", `git commit failed: ${errorMessage(cause)}`, { cause }); |
| 124 | } |
| 125 | return { oid }; |
| 126 | } |
| 127 | |
| 128 | function resolveIdent( |
| 129 | explicit: { name: string; email: string } | undefined, |
no test coverage detected