@abhaydesu/git-pilot 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,440 @@
1
+ import { execa } from "execa";
2
+ import process from "node:process";
3
+
4
+ export const SERVICE_NAME = "git-pilot";
5
+ const SAFE_IDENTIFIER = /^[a-zA-Z0-9._-]+$/;
6
+
7
+ export class CredentialStoreUnavailableError extends Error {
8
+ constructor(message, platform) {
9
+ super(message);
10
+ this.name = "CredentialStoreUnavailableError";
11
+ this.platform = platform;
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Redacts any occurrence of a secret (and its hex representation) or key-shaped token from text.
17
+ */
18
+ export function redactSecretText(text, secrets = []) {
19
+ if (typeof text !== "string" || !text) return "";
20
+ let out = text;
21
+ const envKey = process.env.GEMINI_API_KEY?.trim();
22
+ const allSecrets = [...secrets, envKey].filter((s) => typeof s === "string" && s.length > 0);
23
+
24
+ for (const s of allSecrets) {
25
+ out = out.replaceAll(s, "[REDACTED]");
26
+ const hex = Buffer.from(s, "utf8").toString("hex");
27
+ if (hex.length >= 8) {
28
+ out = out.replaceAll(hex, "[REDACTED]");
29
+ }
30
+ }
31
+
32
+ // Also redact Google API key patterns
33
+ out = out.replace(/\bAIza[0-9A-Za-z-_]{20,}\b/g, "[REDACTED]");
34
+ return out;
35
+ }
36
+
37
+ function assertSafeIdentifier(value, label) {
38
+ if (typeof value !== "string" || !SAFE_IDENTIFIER.test(value)) {
39
+ throw new Error(
40
+ `Invalid ${label}: must contain only alphanumeric, '.', '_', or '-' characters.`
41
+ );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * MemoryCredentialStore provides an in-memory implementation for testing.
47
+ */
48
+ export class MemoryCredentialStore {
49
+ constructor() {
50
+ this.store = new Map();
51
+ this.platform = "memory";
52
+ }
53
+
54
+ async isAvailable() {
55
+ return { available: true };
56
+ }
57
+
58
+ async getPassword(service, account) {
59
+ return this.store.get(`${service}:${account}`) ?? null;
60
+ }
61
+
62
+ async setPassword(service, account, password) {
63
+ this.store.set(`${service}:${account}`, password);
64
+ }
65
+
66
+ async deletePassword(service, account) {
67
+ return this.store.delete(`${service}:${account}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * OsCredentialStore delegates to native OS credential tools without a shell.
73
+ * Secrets are passed exclusively via stdin, never in process argv.
74
+ */
75
+ export class OsCredentialStore {
76
+ constructor(optionsOrPlatform = process.platform) {
77
+ if (typeof optionsOrPlatform === "string") {
78
+ this.platform = optionsOrPlatform;
79
+ this.execFn = execa;
80
+ this.keychainPath = null;
81
+ } else {
82
+ this.platform = optionsOrPlatform?.platform || process.platform;
83
+ this.execFn = optionsOrPlatform?.execFn || execa;
84
+ this.keychainPath = optionsOrPlatform?.keychainPath || null;
85
+ }
86
+ }
87
+
88
+ async isAvailable() {
89
+ if (this.platform === "darwin") {
90
+ try {
91
+ await this.execFn("/usr/bin/security", ["default-keychain"]);
92
+ return { available: true };
93
+ } catch (err) {
94
+ return {
95
+ available: false,
96
+ reason: `macOS Keychain is inaccessible: ${redactSecretText(err.stderr || err.message)}`,
97
+ };
98
+ }
99
+ }
100
+
101
+ if (this.platform === "linux") {
102
+ try {
103
+ await this.execFn("which", ["secret-tool"]);
104
+ return { available: true };
105
+ } catch {
106
+ return {
107
+ available: false,
108
+ reason:
109
+ "Linux Secret Service utility 'secret-tool' was not found. " +
110
+ "Please install libsecret-tools (e.g. `sudo apt install libsecret-tools` or `pacman -S libsecret`) " +
111
+ "and ensure a Secret Service daemon (such as GNOME Keyring or KeePassXC) is running.",
112
+ };
113
+ }
114
+ }
115
+
116
+ if (this.platform === "win32") {
117
+ try {
118
+ await this.execFn("powershell.exe", [
119
+ "-NoProfile",
120
+ "-NonInteractive",
121
+ "-Command",
122
+ "[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null; 'OK'",
123
+ ]);
124
+ return { available: true };
125
+ } catch {
126
+ return {
127
+ available: false,
128
+ reason: "Windows Credential Manager / PasswordVault is unavailable.",
129
+ };
130
+ }
131
+ }
132
+
133
+ return {
134
+ available: false,
135
+ reason: `Unsupported platform for OS credential storage: ${this.platform}`,
136
+ };
137
+ }
138
+
139
+ async getPassword(service, account) {
140
+ assertSafeIdentifier(service, "service");
141
+ assertSafeIdentifier(account, "account");
142
+
143
+ const status = await this.isAvailable();
144
+ if (!status.available) {
145
+ throw new CredentialStoreUnavailableError(status.reason, this.platform);
146
+ }
147
+
148
+ if (this.platform === "darwin") {
149
+ try {
150
+ const args = ["find-generic-password", "-s", service, "-a", account, "-w"];
151
+ if (this.keychainPath) args.push(this.keychainPath);
152
+ const { stdout } = await this.execFn("/usr/bin/security", args);
153
+ return stdout.trim();
154
+ } catch (err) {
155
+ const msg = err.stderr || err.message || "";
156
+ // Exit code 44 on macOS security means item not found
157
+ if (err.exitCode === 44 || msg.includes("could not be found")) {
158
+ return null;
159
+ }
160
+ throw new CredentialStoreUnavailableError(
161
+ `macOS Keychain lookup failed: ${redactSecretText(msg)}`,
162
+ this.platform
163
+ );
164
+ }
165
+ }
166
+
167
+ if (this.platform === "linux") {
168
+ try {
169
+ const { stdout } = await this.execFn("secret-tool", [
170
+ "lookup",
171
+ "service",
172
+ service,
173
+ "account",
174
+ account,
175
+ ]);
176
+ return stdout.trim() || null;
177
+ } catch (err) {
178
+ const msg = err.stderr || err.message || "";
179
+ if (
180
+ err.exitCode === 1 &&
181
+ !msg.toLowerCase().includes("dbus") &&
182
+ !msg.toLowerCase().includes("secret service") &&
183
+ !msg.toLowerCase().includes("cannot autolaunch")
184
+ ) {
185
+ return null;
186
+ }
187
+ throw new CredentialStoreUnavailableError(
188
+ `Linux Secret Service lookup failed: ${redactSecretText(msg || "Secret Service daemon unavailable")}`,
189
+ this.platform
190
+ );
191
+ }
192
+ }
193
+
194
+ if (this.platform === "win32") {
195
+ const script = `
196
+ $service = '${service}';
197
+ $account = '${account}';
198
+ try {
199
+ $vault = New-Object Windows.Security.Credentials.PasswordVault;
200
+ $cred = $vault.Retrieve($service, $account);
201
+ $cred.RetrievePassword();
202
+ [Console]::Write($cred.Password);
203
+ } catch {
204
+ if ($_.Exception.Message -match 'Element not found') { exit 44; }
205
+ [Console]::Error.Write($_.Exception.Message);
206
+ exit 1;
207
+ }
208
+ `;
209
+ try {
210
+ const { stdout } = await this.execFn("powershell.exe", [
211
+ "-NoProfile",
212
+ "-NonInteractive",
213
+ "-Command",
214
+ script,
215
+ ]);
216
+ return stdout.trim() || null;
217
+ } catch (err) {
218
+ if (err.exitCode === 44) return null;
219
+ throw new CredentialStoreUnavailableError(
220
+ `Windows Credential Manager lookup failed: ${redactSecretText(err.stderr || err.message)}`,
221
+ this.platform
222
+ );
223
+ }
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ async setPassword(service, account, password) {
230
+ assertSafeIdentifier(service, "service");
231
+ assertSafeIdentifier(account, "account");
232
+
233
+ const status = await this.isAvailable();
234
+ if (!status.available) {
235
+ throw new CredentialStoreUnavailableError(status.reason, this.platform);
236
+ }
237
+
238
+ try {
239
+ if (this.platform === "darwin") {
240
+ // Use `security -i` (interactive stdin mode) with `-X <hex>` so the secret
241
+ // never appears in process argv (`ps`) and avoids shell/quote parsing issues.
242
+ const hexPassword = Buffer.from(password, "utf8").toString("hex");
243
+ const keychainArg = this.keychainPath
244
+ ? ` "${this.keychainPath.replace(/(["\\])/g, "\\$1")}"`
245
+ : "";
246
+ const commandLine = `add-generic-password -U -s ${service} -a ${account} -X ${hexPassword}${keychainArg}\n`;
247
+ await this.execFn("/usr/bin/security", ["-i"], {
248
+ input: commandLine,
249
+ });
250
+ return;
251
+ }
252
+
253
+ if (this.platform === "linux") {
254
+ // Pass password over stdin to avoid revealing in process arguments
255
+ await this.execFn(
256
+ "secret-tool",
257
+ ["store", `--label=Git Pilot (${account})`, "service", service, "account", account],
258
+ { input: password }
259
+ );
260
+ return;
261
+ }
262
+
263
+ if (this.platform === "win32") {
264
+ const script = `
265
+ $service = '${service}';
266
+ $account = '${account}';
267
+ $pass = [Console]::In.ReadToEnd();
268
+ $vault = New-Object Windows.Security.Credentials.PasswordVault;
269
+ $cred = New-Object Windows.Security.Credentials.PasswordCredential($service, $account, $pass);
270
+ $vault.Add($cred);
271
+ `;
272
+ await this.execFn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
273
+ input: password,
274
+ });
275
+ }
276
+ } catch (err) {
277
+ const sanitized = redactSecretText(
278
+ err.stderr || err.shortMessage || err.message || "Failed to write to OS credential store.",
279
+ [password]
280
+ );
281
+ throw new CredentialStoreUnavailableError(
282
+ `Could not store credential in OS credential store: ${sanitized}`,
283
+ this.platform
284
+ );
285
+ }
286
+ }
287
+
288
+ async deletePassword(service, account) {
289
+ assertSafeIdentifier(service, "service");
290
+ assertSafeIdentifier(account, "account");
291
+
292
+ const status = await this.isAvailable();
293
+ if (!status.available) {
294
+ throw new CredentialStoreUnavailableError(status.reason, this.platform);
295
+ }
296
+
297
+ if (this.platform === "darwin") {
298
+ try {
299
+ const args = ["delete-generic-password", "-s", service, "-a", account];
300
+ if (this.keychainPath) args.push(this.keychainPath);
301
+ await this.execFn("/usr/bin/security", args);
302
+ return true;
303
+ } catch (err) {
304
+ const msg = err.stderr || err.message || "";
305
+ if (err.exitCode === 44 || msg.includes("could not be found")) {
306
+ return false;
307
+ }
308
+ throw new CredentialStoreUnavailableError(
309
+ `macOS Keychain delete failed: ${redactSecretText(msg)}`,
310
+ this.platform
311
+ );
312
+ }
313
+ }
314
+
315
+ if (this.platform === "linux") {
316
+ try {
317
+ await this.execFn("secret-tool", ["clear", "service", service, "account", account]);
318
+ return true;
319
+ } catch (err) {
320
+ if (err.exitCode === 1) return false;
321
+ throw new CredentialStoreUnavailableError(
322
+ `Linux Secret Service delete failed: ${redactSecretText(err.stderr || err.message)}`,
323
+ this.platform
324
+ );
325
+ }
326
+ }
327
+
328
+ if (this.platform === "win32") {
329
+ const script = `
330
+ $service = '${service}';
331
+ $account = '${account}';
332
+ try {
333
+ $vault = New-Object Windows.Security.Credentials.PasswordVault;
334
+ $cred = $vault.Retrieve($service, $account);
335
+ $vault.Remove($cred);
336
+ exit 0;
337
+ } catch {
338
+ if ($_.Exception.Message -match 'Element not found') { exit 44; }
339
+ [Console]::Error.Write($_.Exception.Message);
340
+ exit 1;
341
+ }
342
+ `;
343
+ try {
344
+ await this.execFn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script]);
345
+ return true;
346
+ } catch (err) {
347
+ if (err.exitCode === 44) return false;
348
+ throw new CredentialStoreUnavailableError(
349
+ `Windows Credential Manager delete failed: ${redactSecretText(err.stderr || err.message)}`,
350
+ this.platform
351
+ );
352
+ }
353
+ }
354
+
355
+ return false;
356
+ }
357
+ }
358
+
359
+ let activeBackend = new OsCredentialStore();
360
+
361
+ /** Override active backend (useful for testing) */
362
+ export function setCredentialBackend(backend) {
363
+ activeBackend = backend;
364
+ }
365
+
366
+ export function getCredentialBackend() {
367
+ return activeBackend;
368
+ }
369
+
370
+ export function getEnvVarName(provider = "gemini") {
371
+ return provider === "gemini" ? "GEMINI_API_KEY" : `${provider.toUpperCase()}_API_KEY`;
372
+ }
373
+
374
+ /**
375
+ * Returns the environment-provided API key if set, without touching the OS credential store.
376
+ */
377
+ export function getEnvApiKey(provider = "gemini") {
378
+ const envVarName = getEnvVarName(provider);
379
+ const envVal = process.env[envVarName]?.trim();
380
+ if (envVal) {
381
+ return { key: envVal, source: "env", envVarName };
382
+ }
383
+ return null;
384
+ }
385
+
386
+ /**
387
+ * Returns the API key stored in the OS credential store, ignoring environment variables.
388
+ */
389
+ export async function getStoredApiKey(provider = "gemini") {
390
+ const stored = await activeBackend.getPassword(SERVICE_NAME, provider);
391
+ if (stored) {
392
+ return { key: stored, source: "keychain" };
393
+ }
394
+ return null;
395
+ }
396
+
397
+ /**
398
+ * Returns the effective API key for `provider`.
399
+ * Checks GEMINI_API_KEY env var first for CI/ephemeral use,
400
+ * then checks OS credential store.
401
+ * Returns: { key: string, source: "env" | "keychain" } or null if not set.
402
+ */
403
+ export async function getApiKey(provider = "gemini") {
404
+ const envKey = getEnvApiKey(provider);
405
+ if (envKey) {
406
+ return { key: envKey.key, source: "env" };
407
+ }
408
+ return getStoredApiKey(provider);
409
+ }
410
+
411
+ /**
412
+ * Stores the API key in the OS credential store.
413
+ * Does NOT persist environment variables.
414
+ */
415
+ export async function setApiKey(provider = "gemini", apiKey) {
416
+ if (!apiKey || typeof apiKey !== "string" || apiKey.trim() === "") {
417
+ throw new Error("API key must be a non-empty string.");
418
+ }
419
+ await activeBackend.setPassword(SERVICE_NAME, provider, apiKey.trim());
420
+ }
421
+
422
+ /**
423
+ * Deletes the stored API key from the OS credential store.
424
+ */
425
+ export async function deleteApiKey(provider = "gemini") {
426
+ return activeBackend.deletePassword(SERVICE_NAME, provider);
427
+ }
428
+
429
+ /**
430
+ * Masks an API key for safe display in logs and status commands.
431
+ * Examples: AIzaSy...1234 -> AIza...1234
432
+ */
433
+ export function maskApiKey(key) {
434
+ if (!key || typeof key !== "string") return "****";
435
+ const trimmed = key.trim();
436
+ if (trimmed.length <= 8) return "****";
437
+ const prefix = trimmed.slice(0, 4);
438
+ const suffix = trimmed.slice(-4);
439
+ return `${prefix}...${suffix}`;
440
+ }
package/src/lib/exec.js CHANGED
@@ -1,8 +1,59 @@
1
1
  import { parse } from "shell-quote";
2
2
  import { execa } from "execa";
3
+ import { getSanitizedGitEnv } from "./git.js";
3
4
 
4
5
  export class UnsafeCommandError extends Error {}
5
6
 
7
+ // Mirrors the API's allowlist. The CLI re-checks it because the API (or a custom
8
+ // GIT_PILOT_API_URL) is an untrusted source of commands. This also rejects leading
9
+ // global options such as `git -c core.sshCommand=... fetch`.
10
+ const ALLOWED_SUBCOMMANDS = new Set([
11
+ "add",
12
+ "branch",
13
+ "checkout",
14
+ "cherry-pick",
15
+ "clone",
16
+ "commit",
17
+ "diff",
18
+ "fetch",
19
+ "log",
20
+ "merge",
21
+ "pull",
22
+ "push",
23
+ "rebase",
24
+ "reflog",
25
+ "reset",
26
+ "restore",
27
+ "revert",
28
+ "show",
29
+ "stash",
30
+ "status",
31
+ "switch",
32
+ "tag",
33
+ ]);
34
+
35
+ // Options that make git run another program or write to an arbitrary path.
36
+ const FORBIDDEN_OPTION =
37
+ /^--(upload-pack|receive-pack|exec|exec-path|output|ext-diff|textconv)(=|$)/;
38
+
39
+ function assertSafeArgs(args) {
40
+ const [subcommand, ...rest] = args;
41
+ if (!ALLOWED_SUBCOMMANDS.has(subcommand)) {
42
+ throw new UnsafeCommandError(`"${subcommand}" is not an allowed git subcommand.`);
43
+ }
44
+
45
+ for (const arg of rest) {
46
+ if (arg === "--") break; // everything after is a path, not an option
47
+ if (
48
+ FORBIDDEN_OPTION.test(arg) ||
49
+ (subcommand === "rebase" && arg.startsWith("-x")) || // -x = --exec
50
+ (subcommand === "clone" && arg.startsWith("-u")) // -u = --upload-pack
51
+ ) {
52
+ throw new UnsafeCommandError(`The option "${arg}" is not allowed.`);
53
+ }
54
+ }
55
+ }
56
+
6
57
  /**
7
58
  * Turns a suggested command string into an argv array for `git`, or throws.
8
59
  * Only plain `git <args...>` is accepted: quoting is honored, but shell
@@ -27,8 +78,15 @@ export function parseGitCommand(command) {
27
78
  if (tokens[0] !== "git" || tokens.length < 2) {
28
79
  throw new UnsafeCommandError("Only git commands can be executed.");
29
80
  }
30
- return tokens.slice(1);
81
+ const args = tokens.slice(1);
82
+ assertSafeArgs(args);
83
+ return args;
31
84
  }
32
85
 
33
- /** Runs `git <args>` with the terminal attached. */
34
- export const runGit = (args) => execa("git", args, { stdio: "inherit" });
86
+ /** Runs `git <args>` with the terminal attached and provider secrets stripped from env. */
87
+ export const runGit = (args) =>
88
+ execa("git", args, {
89
+ stdio: "inherit",
90
+ env: getSanitizedGitEnv(),
91
+ extendEnv: false,
92
+ });
package/src/lib/git.js CHANGED
@@ -1,13 +1,109 @@
1
1
  import { execa } from "execa";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import process from "node:process";
5
+ import { sanitizeTerminalText } from "./sanitizer.js";
6
+ import { redactSecretText } from "./credentials.js";
4
7
 
5
- const git = async (args) => (await execa("git", args)).stdout;
8
+ const SECRET_ENV_PATTERN =
9
+ /^(GEMINI|GOOGLE|OPENAI|ANTHROPIC|CLAUDE|COHERE|MISTRAL|GIT_PILOT)_.*(KEY|TOKEN|SECRET)|_API_KEY$/i;
10
+
11
+ /**
12
+ * Builds a clean environment for Git subprocesses and hooks.
13
+ * Strips GEMINI_API_KEY and other provider credentials so Git hooks or helpers
14
+ * never inherit API keys, while preserving standard Git and system variables.
15
+ */
16
+ export function getSanitizedGitEnv(baseEnv = process.env) {
17
+ const clean = {};
18
+ for (const [key, value] of Object.entries(baseEnv)) {
19
+ if (value === undefined) continue;
20
+ if (SECRET_ENV_PATTERN.test(key)) continue;
21
+ if (key === "GIT_EXTERNAL_DIFF") continue;
22
+ clean[key] = value;
23
+ }
24
+ clean.GIT_PAGER = "cat";
25
+ return clean;
26
+ }
27
+
28
+ const git = async (args, opts = {}) =>
29
+ (
30
+ await execa("git", args, {
31
+ env: getSanitizedGitEnv(),
32
+ extendEnv: false,
33
+ ...opts,
34
+ })
35
+ ).stdout;
36
+
37
+ export class GitCommitError extends Error {
38
+ constructor(message, originalError, commitMessage) {
39
+ super(message);
40
+ this.name = "GitCommitError";
41
+ this.originalError = originalError;
42
+ this.commitMessage = commitMessage;
43
+ this.exitCode = originalError?.exitCode;
44
+ }
45
+ }
46
+
47
+ export async function isInsideGitRepo() {
48
+ try {
49
+ const res = await git(["rev-parse", "--is-inside-work-tree"]);
50
+ return res.trim() === "true";
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ export const getStagedDiff = () => git(["diff", "--staged", "--no-ext-diff", "--no-textconv"]);
57
+
58
+ export const getStagedSummary = () =>
59
+ git(["diff", "--staged", "--name-status", "--no-ext-diff", "--no-textconv"]);
60
+
61
+ /**
62
+ * Returns staged file paths using NUL-delimited (`-z`) output so filenames
63
+ * containing leading/trailing spaces, special characters, or newlines remain intact.
64
+ */
65
+ export async function getStagedFiles() {
66
+ const output = await git(
67
+ ["diff", "--staged", "--name-only", "-z", "--no-ext-diff", "--no-textconv"],
68
+ { stripFinalNewline: false }
69
+ );
70
+ return output.split("\0").filter((s) => s.length > 0);
71
+ }
72
+
73
+ /**
74
+ * Returns the current staged index tree hash (`git write-tree`).
75
+ * Used for best-effort detection of concurrent index modifications before `git commit`.
76
+ */
77
+ export async function getStagedTreeHash() {
78
+ try {
79
+ return (await git(["write-tree"])).trim();
80
+ } catch {
81
+ // Fallback if write-tree cannot run (e.g. unmerged paths)
82
+ return (await getStagedDiff()).trim();
83
+ }
84
+ }
6
85
 
7
- export const getStagedDiff = () => git(["diff", "--staged"]);
8
- export const getStagedSummary = () => git(["diff", "--staged", "--name-status"]);
9
86
  export const getReflog = () => git(["reflog", "-n", "15"]);
10
- export const commit = (message) => git(["commit", "-m", message]);
87
+
88
+ export async function hasOrigHead() {
89
+ try {
90
+ await git(["rev-parse", "--verify", "--quiet", "ORIG_HEAD^{commit}"]);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ export async function commit(message) {
98
+ try {
99
+ return await git(["commit", "-m", message]);
100
+ } catch (err) {
101
+ const rawDetails = (err.stderr || err.stdout || err.message || "").trim();
102
+ const details = sanitizeTerminalText(redactSecretText(rawDetails));
103
+ throw new GitCommitError(`Git commit failed (hooks or git error): ${details}`, err, message);
104
+ }
105
+ }
106
+
11
107
  export const createBranch = (name) => git(["switch", "-c", name]);
12
108
 
13
109
  const exists = (p) =>
@@ -16,12 +112,34 @@ const exists = (p) =>
16
112
  () => false
17
113
  );
18
114
 
115
+ export async function getGitRepoState() {
116
+ try {
117
+ const gitDir = path.resolve(await git(["rev-parse", "--git-dir"]));
118
+ const states = [];
119
+ if (
120
+ (await exists(path.join(gitDir, "rebase-apply"))) ||
121
+ (await exists(path.join(gitDir, "rebase-merge")))
122
+ ) {
123
+ states.push("rebase");
124
+ }
125
+ if (await exists(path.join(gitDir, "MERGE_HEAD"))) {
126
+ states.push("merge");
127
+ }
128
+ if (await exists(path.join(gitDir, "CHERRY_PICK_HEAD"))) {
129
+ states.push("cherry-pick");
130
+ }
131
+ if (await exists(path.join(gitDir, "REVERT_HEAD"))) {
132
+ states.push("revert");
133
+ }
134
+ return states;
135
+ } catch {
136
+ return [];
137
+ }
138
+ }
139
+
19
140
  export async function isRebaseInProgress() {
20
- const gitDir = path.resolve(await git(["rev-parse", "--git-dir"]));
21
- return (
22
- (await exists(path.join(gitDir, "rebase-apply"))) ||
23
- (await exists(path.join(gitDir, "rebase-merge")))
24
- );
141
+ const states = await getGitRepoState();
142
+ return states.includes("rebase");
25
143
  }
26
144
 
27
145
  /** Lowercase, hyphenated, git-safe branch name; "" if nothing usable is left. */