@g-abhishek/gitx 0.1.1 → 0.1.4
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.
- package/README.md +374 -3
- package/dist/ai/claudeAi.d.ts +35 -0
- package/dist/ai/claudeAi.d.ts.map +1 -0
- package/dist/ai/claudeAi.js +396 -0
- package/dist/ai/claudeAi.js.map +1 -0
- package/dist/ai/claudeCliAi.d.ts +27 -0
- package/dist/ai/claudeCliAi.d.ts.map +1 -0
- package/dist/ai/claudeCliAi.js +312 -0
- package/dist/ai/claudeCliAi.js.map +1 -0
- package/dist/ai/localClaudeAi.d.ts +2 -0
- package/dist/ai/localClaudeAi.d.ts.map +1 -0
- package/dist/ai/localClaudeAi.js +4 -0
- package/dist/ai/localClaudeAi.js.map +1 -0
- package/dist/ai/mockAi.d.ts +8 -1
- package/dist/ai/mockAi.d.ts.map +1 -1
- package/dist/ai/mockAi.js +57 -0
- package/dist/ai/mockAi.js.map +1 -1
- package/dist/ai/openAiAi.d.ts +33 -0
- package/dist/ai/openAiAi.d.ts.map +1 -0
- package/dist/ai/openAiAi.js +388 -0
- package/dist/ai/openAiAi.js.map +1 -0
- package/dist/ai/reviewHelpers.d.ts +66 -0
- package/dist/ai/reviewHelpers.d.ts.map +1 -0
- package/dist/ai/reviewHelpers.js +559 -0
- package/dist/ai/reviewHelpers.js.map +1 -0
- package/dist/ai/types.d.ts +247 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/ai/types.js.map +1 -1
- package/dist/cli/commands/ask.d.ts +27 -0
- package/dist/cli/commands/ask.d.ts.map +1 -0
- package/dist/cli/commands/ask.js +230 -0
- package/dist/cli/commands/ask.js.map +1 -0
- package/dist/cli/commands/commit.d.ts +16 -0
- package/dist/cli/commands/commit.d.ts.map +1 -0
- package/dist/cli/commands/commit.js +163 -0
- package/dist/cli/commands/commit.js.map +1 -0
- package/dist/cli/commands/config.d.ts +4 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +666 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/implement.d.ts.map +1 -1
- package/dist/cli/commands/implement.js +149 -28
- package/dist/cli/commands/implement.js.map +1 -1
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -54
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/port.d.ts +32 -0
- package/dist/cli/commands/port.d.ts.map +1 -0
- package/dist/cli/commands/port.js +554 -0
- package/dist/cli/commands/port.js.map +1 -0
- package/dist/cli/commands/pr/close.d.ts +15 -0
- package/dist/cli/commands/pr/close.d.ts.map +1 -0
- package/dist/cli/commands/pr/close.js +71 -0
- package/dist/cli/commands/pr/close.js.map +1 -0
- package/dist/cli/commands/pr/create.d.ts +17 -0
- package/dist/cli/commands/pr/create.d.ts.map +1 -1
- package/dist/cli/commands/pr/create.js +209 -5
- package/dist/cli/commands/pr/create.js.map +1 -1
- package/dist/cli/commands/pr/fixComments.d.ts.map +1 -1
- package/dist/cli/commands/pr/fixComments.js +77 -5
- package/dist/cli/commands/pr/fixComments.js.map +1 -1
- package/dist/cli/commands/pr/index.d.ts.map +1 -1
- package/dist/cli/commands/pr/index.js +4 -0
- package/dist/cli/commands/pr/index.js.map +1 -1
- package/dist/cli/commands/pr/list.d.ts.map +1 -1
- package/dist/cli/commands/pr/list.js +26 -3
- package/dist/cli/commands/pr/list.js.map +1 -1
- package/dist/cli/commands/pr/merge.d.ts +23 -0
- package/dist/cli/commands/pr/merge.d.ts.map +1 -0
- package/dist/cli/commands/pr/merge.js +191 -0
- package/dist/cli/commands/pr/merge.js.map +1 -0
- package/dist/cli/commands/pr/review.d.ts.map +1 -1
- package/dist/cli/commands/pr/review.js +123 -5
- package/dist/cli/commands/pr/review.js.map +1 -1
- package/dist/cli/commands/push.d.ts +16 -0
- package/dist/cli/commands/push.d.ts.map +1 -0
- package/dist/cli/commands/push.js +166 -0
- package/dist/cli/commands/push.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +24 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +414 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +34 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config.d.ts +20 -3
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +103 -24
- package/dist/config/config.js.map +1 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +70 -9
- package/dist/config/schema.js.map +1 -1
- package/dist/core/context.d.ts +13 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +2 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/gitx.d.ts +47 -0
- package/dist/core/gitx.d.ts.map +1 -1
- package/dist/core/gitx.js +204 -9
- package/dist/core/gitx.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/azure.d.ts +26 -0
- package/dist/providers/azure.d.ts.map +1 -0
- package/dist/providers/azure.js +256 -0
- package/dist/providers/azure.js.map +1 -0
- package/dist/providers/base.d.ts +104 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +5 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/factory.d.ts +8 -0
- package/dist/providers/factory.d.ts.map +1 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/github.d.ts +19 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +291 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/gitlab.d.ts +19 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +186 -0
- package/dist/providers/gitlab.js.map +1 -0
- package/dist/types/config.d.ts +53 -9
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/utils/azureAuth.d.ts +51 -0
- package/dist/utils/azureAuth.d.ts.map +1 -0
- package/dist/utils/azureAuth.js +172 -0
- package/dist/utils/azureAuth.js.map +1 -0
- package/dist/utils/git.d.ts +22 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +63 -7
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/gitOps.d.ts +118 -0
- package/dist/utils/gitOps.d.ts.map +1 -0
- package/dist/utils/gitOps.js +380 -0
- package/dist/utils/gitOps.js.map +1 -0
- package/dist/utils/lockFile.d.ts +13 -0
- package/dist/utils/lockFile.d.ts.map +1 -0
- package/dist/utils/lockFile.js +54 -0
- package/dist/utils/lockFile.js.map +1 -0
- package/dist/utils/retry.d.ts +10 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +31 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/workflows/implement.d.ts +41 -0
- package/dist/workflows/implement.d.ts.map +1 -0
- package/dist/workflows/implement.js +219 -0
- package/dist/workflows/implement.js.map +1 -0
- package/dist/workflows/pr.d.ts +41 -0
- package/dist/workflows/pr.d.ts.map +1 -0
- package/dist/workflows/pr.js +285 -0
- package/dist/workflows/pr.js.map +1 -0
- package/dist/workflows/prAddress.d.ts +55 -0
- package/dist/workflows/prAddress.d.ts.map +1 -0
- package/dist/workflows/prAddress.js +349 -0
- package/dist/workflows/prAddress.js.map +1 -0
- package/package.json +1 -1
package/dist/utils/git.js
CHANGED
|
@@ -11,24 +11,67 @@ export async function getGitRemoteOriginUrl(cwd = process.cwd()) {
|
|
|
11
11
|
return undefined;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
export async function isInsideGitRepo(cwd = process.cwd()) {
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
17
|
+
return String(stdout ?? "").trim() === "true";
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Infer a "owner/repo" slug from a git remote URL.
|
|
25
|
+
*
|
|
26
|
+
* Handles:
|
|
27
|
+
* GitHub / GitLab:
|
|
28
|
+
* git@github.com:owner/repo.git
|
|
29
|
+
* https://github.com/owner/repo(.git)
|
|
30
|
+
* ssh://git@github.com/owner/repo(.git)
|
|
31
|
+
*
|
|
32
|
+
* Azure DevOps:
|
|
33
|
+
* https://dev.azure.com/org/project/_git/repo
|
|
34
|
+
* https://org.visualstudio.com/project/_git/repo
|
|
35
|
+
* git@ssh.dev.azure.com:v3/org/project/repo (new SSH format)
|
|
36
|
+
* org@vs-ssh.visualstudio.com:v3/org/project/repo (legacy SSH format)
|
|
37
|
+
*
|
|
38
|
+
* Returns:
|
|
39
|
+
* GitHub/GitLab: "owner/repo"
|
|
40
|
+
* Azure: "org/project/repo"
|
|
41
|
+
*/
|
|
14
42
|
export function inferRepoSlugFromRemote(url) {
|
|
15
|
-
// Supports common GitHub/GitLab URL formats:
|
|
16
|
-
// - git@github.com:owner/repo.git
|
|
17
|
-
// - https://github.com/owner/repo(.git)
|
|
18
|
-
// - ssh://git@github.com/owner/repo(.git)
|
|
19
43
|
const cleaned = url.trim();
|
|
20
|
-
|
|
44
|
+
// ── GitHub / GitLab ──────────────────────────────────────────────────────
|
|
45
|
+
const ghGlPatterns = [
|
|
21
46
|
/^(?:git@)(?:github\.com|gitlab\.com):(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i,
|
|
22
47
|
/^(?:https?:\/\/)(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?(?:\/)?$/i,
|
|
23
|
-
/^(?:ssh:\/\/)(?:git@)(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i
|
|
48
|
+
/^(?:ssh:\/\/)(?:git@)?(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i,
|
|
24
49
|
];
|
|
25
|
-
for (const re of
|
|
50
|
+
for (const re of ghGlPatterns) {
|
|
26
51
|
const match = re.exec(cleaned);
|
|
27
52
|
const owner = match?.groups?.["owner"];
|
|
28
53
|
const repo = match?.groups?.["repo"];
|
|
29
54
|
if (owner && repo)
|
|
30
55
|
return `${owner}/${repo}`;
|
|
31
56
|
}
|
|
57
|
+
// ── Azure DevOps HTTPS ────────────────────────────────────────────────────
|
|
58
|
+
// https://dev.azure.com/{org}/{project}/_git/{repo}
|
|
59
|
+
const azHttps = /https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?(?:\/)?$/i.exec(cleaned);
|
|
60
|
+
if (azHttps)
|
|
61
|
+
return `${azHttps[1]}/${azHttps[2]}/${azHttps[3]}`;
|
|
62
|
+
// https://{org}.visualstudio.com/{project}/_git/{repo}
|
|
63
|
+
const azVs = /https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+?)(?:\.git)?(?:\/)?$/i.exec(cleaned);
|
|
64
|
+
if (azVs)
|
|
65
|
+
return `${azVs[1]}/${azVs[2]}/${azVs[3]}`;
|
|
66
|
+
// ── Azure DevOps SSH ──────────────────────────────────────────────────────
|
|
67
|
+
// New format: git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
|
|
68
|
+
const azSshNew = /^[^@]+@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(cleaned);
|
|
69
|
+
if (azSshNew)
|
|
70
|
+
return `${azSshNew[1]}/${azSshNew[2]}/${azSshNew[3]}`;
|
|
71
|
+
// Legacy format: {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
|
|
72
|
+
const azSshLegacy = /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(cleaned);
|
|
73
|
+
if (azSshLegacy)
|
|
74
|
+
return `${azSshLegacy[1]}/${azSshLegacy[2]}/${azSshLegacy[3]}`;
|
|
32
75
|
return undefined;
|
|
33
76
|
}
|
|
34
77
|
export async function resolveRepoSlugFromCwd(cwd = process.cwd()) {
|
|
@@ -37,4 +80,17 @@ export async function resolveRepoSlugFromCwd(cwd = process.cwd()) {
|
|
|
37
80
|
return undefined;
|
|
38
81
|
return inferRepoSlugFromRemote(originUrl);
|
|
39
82
|
}
|
|
83
|
+
export function detectProviderFromRemote(url) {
|
|
84
|
+
const cleaned = url.trim();
|
|
85
|
+
if (cleaned.includes("github.com"))
|
|
86
|
+
return "github";
|
|
87
|
+
if (cleaned.includes("gitlab.com"))
|
|
88
|
+
return "gitlab";
|
|
89
|
+
if (cleaned.includes("dev.azure.com") || // HTTPS + new SSH (ssh.dev.azure.com)
|
|
90
|
+
cleaned.includes("visualstudio.com") // legacy HTTPS + legacy SSH (vs-ssh.visualstudio.com)
|
|
91
|
+
) {
|
|
92
|
+
return "azure";
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
40
96
|
//# sourceMappingURL=git.js.map
|
package/dist/utils/git.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { ProviderKind } from \"../types/provider.js\";\n\nconst execFileAsync = promisify(execFile);\n\nexport async function getGitRemoteOriginUrl(cwd = process.cwd()): Promise<string | undefined> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"config\", \"--get\", \"remote.origin.url\"], { cwd });\n const url = String(stdout ?? \"\").trim();\n return url.length > 0 ? url : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport async function isInsideGitRepo(cwd = process.cwd()): Promise<boolean> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], { cwd });\n return String(stdout ?? \"\").trim() === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Infer a \"owner/repo\" slug from a git remote URL.\n *\n * Handles:\n * GitHub / GitLab:\n * git@github.com:owner/repo.git\n * https://github.com/owner/repo(.git)\n * ssh://git@github.com/owner/repo(.git)\n *\n * Azure DevOps:\n * https://dev.azure.com/org/project/_git/repo\n * https://org.visualstudio.com/project/_git/repo\n * git@ssh.dev.azure.com:v3/org/project/repo (new SSH format)\n * org@vs-ssh.visualstudio.com:v3/org/project/repo (legacy SSH format)\n *\n * Returns:\n * GitHub/GitLab: \"owner/repo\"\n * Azure: \"org/project/repo\"\n */\nexport function inferRepoSlugFromRemote(url: string): string | undefined {\n const cleaned = url.trim();\n\n // ── GitHub / GitLab ──────────────────────────────────────────────────────\n const ghGlPatterns: RegExp[] = [\n /^(?:git@)(?:github\\.com|gitlab\\.com):(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?$/i,\n /^(?:https?:\\/\\/)(?:github\\.com|gitlab\\.com)\\/(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?(?:\\/)?$/i,\n /^(?:ssh:\\/\\/)(?:git@)?(?:github\\.com|gitlab\\.com)\\/(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?$/i,\n ];\n\n for (const re of ghGlPatterns) {\n const match = re.exec(cleaned);\n const owner = match?.groups?.[\"owner\"];\n const repo = match?.groups?.[\"repo\"];\n if (owner && repo) return `${owner}/${repo}`;\n }\n\n // ── Azure DevOps HTTPS ────────────────────────────────────────────────────\n // https://dev.azure.com/{org}/{project}/_git/{repo}\n const azHttps = /https?:\\/\\/dev\\.azure\\.com\\/([^/]+)\\/([^/]+)\\/_git\\/([^/]+?)(?:\\.git)?(?:\\/)?$/i.exec(cleaned);\n if (azHttps) return `${azHttps[1]}/${azHttps[2]}/${azHttps[3]}`;\n\n // https://{org}.visualstudio.com/{project}/_git/{repo}\n const azVs = /https?:\\/\\/([^.]+)\\.visualstudio\\.com\\/([^/]+)\\/_git\\/([^/]+?)(?:\\.git)?(?:\\/)?$/i.exec(cleaned);\n if (azVs) return `${azVs[1]}/${azVs[2]}/${azVs[3]}`;\n\n // ── Azure DevOps SSH ──────────────────────────────────────────────────────\n // New format: git@ssh.dev.azure.com:v3/{org}/{project}/{repo}\n const azSshNew = /^[^@]+@ssh\\.dev\\.azure\\.com:v3\\/([^/]+)\\/([^/]+)\\/([^/]+?)(?:\\.git)?$/i.exec(cleaned);\n if (azSshNew) return `${azSshNew[1]}/${azSshNew[2]}/${azSshNew[3]}`;\n\n // Legacy format: {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}\n const azSshLegacy = /^[^@]+@vs-ssh\\.visualstudio\\.com:v3\\/([^/]+)\\/([^/]+)\\/([^/]+?)(?:\\.git)?$/i.exec(cleaned);\n if (azSshLegacy) return `${azSshLegacy[1]}/${azSshLegacy[2]}/${azSshLegacy[3]}`;\n\n return undefined;\n}\n\nexport async function resolveRepoSlugFromCwd(cwd = process.cwd()): Promise<string | undefined> {\n const originUrl = await getGitRemoteOriginUrl(cwd);\n if (!originUrl) return undefined;\n return inferRepoSlugFromRemote(originUrl);\n}\n\nexport function detectProviderFromRemote(url: string): ProviderKind | undefined {\n const cleaned = url.trim();\n if (cleaned.includes(\"github.com\")) return \"github\";\n if (cleaned.includes(\"gitlab.com\")) return \"gitlab\";\n if (\n cleaned.includes(\"dev.azure.com\") || // HTTPS + new SSH (ssh.dev.azure.com)\n cleaned.includes(\"visualstudio.com\") // legacy HTTPS + legacy SSH (vs-ssh.visualstudio.com)\n ) {\n return \"azure\";\n }\n return undefined;\n}\n"]}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended git operations for the implement workflow.
|
|
3
|
+
* All functions execute native git commands via child_process.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getCurrentBranch(cwd?: string): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the default branch (main/master/develop) by inspecting the remote.
|
|
8
|
+
* Falls back to "main" if nothing can be determined.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getDefaultBranchFromGit(cwd?: string, configuredDefault?: string): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Create and checkout a new branch.
|
|
13
|
+
* If the branch already exists, just check it out.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createAndCheckoutBranch(branchName: string, cwd?: string): Promise<void>;
|
|
16
|
+
/** Sanitise a free-form task string into a valid branch name */
|
|
17
|
+
export declare function slugifyBranchName(task: string, prefix?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Write content to a file inside the repo, creating parent directories as needed.
|
|
20
|
+
* Paths should be relative to `cwd`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function writeRepoFile(relativePath: string, content: string, cwd?: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Apply a unified diff string using `git apply`.
|
|
25
|
+
* Returns `true` if applied cleanly, `false` if it failed (caller decides how
|
|
26
|
+
* to handle partial failures).
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyUnifiedDiff(unifiedDiff: string, cwd?: string): Promise<{
|
|
29
|
+
ok: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function stageAll(cwd?: string): Promise<void>;
|
|
33
|
+
export declare function hasStagedChanges(cwd?: string): Promise<boolean>;
|
|
34
|
+
export declare function commitChanges(message: string, cwd?: string): Promise<string>;
|
|
35
|
+
export declare function pushBranch(branchName: string, cwd?: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* List all tracked files in the repo (respects .gitignore).
|
|
38
|
+
* Returns paths relative to `cwd`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function listTrackedFiles(cwd?: string): Promise<string[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Get the unified diff of only staged changes (`git diff --cached`).
|
|
43
|
+
* Use this when the caller wants to commit only what's already in the index.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getStagedDiff(cwd?: string): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Get a compact stat summary of only staged changes (`git diff --cached --stat`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function getStagedDiffStat(cwd?: string): Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Get the unified diff of all uncommitted changes (staged + unstaged).
|
|
52
|
+
*/
|
|
53
|
+
export declare function getWorkingDiff(cwd?: string): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Get a compact summary of staged changes (--stat format):
|
|
56
|
+
* lists every changed file with insertion/deletion counts.
|
|
57
|
+
* Always small regardless of diff size — used to give the AI the
|
|
58
|
+
* complete picture of what changed even when the full diff is truncated.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getWorkingDiffStat(cwd?: string): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Auto-detect the most likely base branch for the current feature branch.
|
|
63
|
+
*
|
|
64
|
+
* Strategy:
|
|
65
|
+
* 1. Check if the current branch has a configured upstream tracking branch.
|
|
66
|
+
* 2. Otherwise, try common default branch names (main, master, develop, dev).
|
|
67
|
+
* 3. For each candidate, count commits on HEAD that are NOT in that branch.
|
|
68
|
+
* The candidate with the fewest such commits is the likely origin.
|
|
69
|
+
*
|
|
70
|
+
* Falls back to "main" if nothing can be determined.
|
|
71
|
+
*/
|
|
72
|
+
export declare function detectBaseBranch(cwd?: string): Promise<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Get the one-line commit log for commits on HEAD that are not in baseBranch.
|
|
75
|
+
* Used to give AI context about what this branch adds.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getBranchCommits(cwd?: string, baseBranch?: string): Promise<string[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Get the unified diff of all changes between baseBranch and HEAD.
|
|
80
|
+
* This is what the PR reviewer would see — all additions across all commits.
|
|
81
|
+
*/
|
|
82
|
+
export declare function getBranchDiff(cwd?: string, baseBranch?: string): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Get the --stat summary of all changes between baseBranch and HEAD.
|
|
85
|
+
* Lists every file changed with insertion/deletion counts.
|
|
86
|
+
* Used alongside a truncated diff so the AI sees the full file list even
|
|
87
|
+
* when the detailed patch is cut off.
|
|
88
|
+
*/
|
|
89
|
+
export declare function getBranchStat(cwd?: string, baseBranch?: string): Promise<string>;
|
|
90
|
+
/**
|
|
91
|
+
* Read file content as a string. Returns empty string if file doesn't exist.
|
|
92
|
+
*/
|
|
93
|
+
export declare function readRepoFile(relativePath: string, cwd?: string): Promise<string | undefined>;
|
|
94
|
+
/**
|
|
95
|
+
* Check whether a branch exists on the remote (origin).
|
|
96
|
+
* Uses git ls-remote which does not require a full fetch.
|
|
97
|
+
*/
|
|
98
|
+
export declare function branchExistsOnRemote(branchName: string, cwd?: string): Promise<boolean>;
|
|
99
|
+
/**
|
|
100
|
+
* Returns true if the working tree has uncommitted changes (staged or unstaged).
|
|
101
|
+
*/
|
|
102
|
+
export declare function isWorkingTreeDirty(cwd?: string): Promise<boolean>;
|
|
103
|
+
/**
|
|
104
|
+
* Returns `git status --short` output for use in AI context.
|
|
105
|
+
* Returns an empty string when the working tree is clean or git fails.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getGitStatus(cwd?: string): Promise<string>;
|
|
108
|
+
/**
|
|
109
|
+
* Returns the last `count` commits as one-line summaries (hash + subject).
|
|
110
|
+
* Defaults to the last 10 commits.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getRecentCommits(cwd?: string, count?: number): Promise<string[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Returns the list of git stashes as `stash@{0}: ...` strings.
|
|
115
|
+
* Returns an empty array when there are no stashes or git fails.
|
|
116
|
+
*/
|
|
117
|
+
export declare function getStashList(cwd?: string): Promise<string[]>;
|
|
118
|
+
//# sourceMappingURL=gitOps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitOps.d.ts","sourceRoot":"","sources":["../../src/utils/gitOps.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended git operations for the implement workflow.
|
|
3
|
+
* All functions execute native git commands via child_process.
|
|
4
|
+
*/
|
|
5
|
+
import { execFile, exec } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
8
|
+
import { dirname, resolve, join } from "node:path";
|
|
9
|
+
import { GitxError } from "./errors.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
// ─── Internal helper ──────────────────────────────────────────────────────────
|
|
13
|
+
async function git(args, cwd) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
16
|
+
return String(stdout ?? "").trim();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const stderr = err.stderr ??
|
|
20
|
+
err.message ??
|
|
21
|
+
String(err);
|
|
22
|
+
throw new GitxError(`git ${args[0]} failed: ${stderr.trim()}`, {
|
|
23
|
+
exitCode: 1,
|
|
24
|
+
cause: err,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// ─── Branch operations ────────────────────────────────────────────────────────
|
|
29
|
+
export async function getCurrentBranch(cwd = process.cwd()) {
|
|
30
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the default branch (main/master/develop) by inspecting the remote.
|
|
34
|
+
* Falls back to "main" if nothing can be determined.
|
|
35
|
+
*/
|
|
36
|
+
export async function getDefaultBranchFromGit(cwd = process.cwd(), configuredDefault) {
|
|
37
|
+
if (configuredDefault)
|
|
38
|
+
return configuredDefault;
|
|
39
|
+
try {
|
|
40
|
+
// Try to read from remote HEAD reference
|
|
41
|
+
const out = await git(["rev-parse", "--abbrev-ref", "origin/HEAD"], cwd);
|
|
42
|
+
// "origin/main" → "main"
|
|
43
|
+
return out.replace(/^origin\//, "") || "main";
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Fall back to checking common branch names
|
|
47
|
+
try {
|
|
48
|
+
const branches = await git(["branch", "-r", "--format=%(refname:short)"], cwd);
|
|
49
|
+
const candidates = ["origin/main", "origin/master", "origin/develop"];
|
|
50
|
+
for (const candidate of candidates) {
|
|
51
|
+
if (branches.split("\n").some((b) => b.trim() === candidate)) {
|
|
52
|
+
return candidate.replace("origin/", "");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* ignore */
|
|
58
|
+
}
|
|
59
|
+
return "main";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create and checkout a new branch.
|
|
64
|
+
* If the branch already exists, just check it out.
|
|
65
|
+
*/
|
|
66
|
+
export async function createAndCheckoutBranch(branchName, cwd = process.cwd()) {
|
|
67
|
+
try {
|
|
68
|
+
await git(["checkout", "-b", branchName], cwd);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Branch might already exist – try checking it out
|
|
72
|
+
await git(["checkout", branchName], cwd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Sanitise a free-form task string into a valid branch name */
|
|
76
|
+
export function slugifyBranchName(task, prefix = "gitx") {
|
|
77
|
+
const slug = task
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
80
|
+
.replace(/^-+|-+$/g, "")
|
|
81
|
+
.slice(0, 50);
|
|
82
|
+
const ts = Date.now().toString(36);
|
|
83
|
+
return `${prefix}/${slug}-${ts}`;
|
|
84
|
+
}
|
|
85
|
+
// ─── File operations ──────────────────────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* Write content to a file inside the repo, creating parent directories as needed.
|
|
88
|
+
* Paths should be relative to `cwd`.
|
|
89
|
+
*/
|
|
90
|
+
export async function writeRepoFile(relativePath, content, cwd = process.cwd()) {
|
|
91
|
+
const abs = resolve(join(cwd, relativePath));
|
|
92
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
93
|
+
await writeFile(abs, content, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
// ─── Diff application ─────────────────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Apply a unified diff string using `git apply`.
|
|
98
|
+
* Returns `true` if applied cleanly, `false` if it failed (caller decides how
|
|
99
|
+
* to handle partial failures).
|
|
100
|
+
*/
|
|
101
|
+
export async function applyUnifiedDiff(unifiedDiff, cwd = process.cwd()) {
|
|
102
|
+
try {
|
|
103
|
+
// Use --3way to handle minor conflicts gracefully
|
|
104
|
+
const { stdout, stderr } = await execAsync(`echo ${JSON.stringify(unifiedDiff)} | git apply --3way --whitespace=fix -`, { cwd });
|
|
105
|
+
return { ok: true, error: stderr || stdout || undefined };
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const stderr = err.stderr ??
|
|
109
|
+
err.message ??
|
|
110
|
+
String(err);
|
|
111
|
+
return { ok: false, error: stderr.trim() };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ─── Staging & committing ─────────────────────────────────────────────────────
|
|
115
|
+
export async function stageAll(cwd = process.cwd()) {
|
|
116
|
+
await git(["add", "-A"], cwd);
|
|
117
|
+
}
|
|
118
|
+
export async function hasStagedChanges(cwd = process.cwd()) {
|
|
119
|
+
try {
|
|
120
|
+
const out = await git(["diff", "--cached", "--name-only"], cwd);
|
|
121
|
+
return out.trim().length > 0;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export async function commitChanges(message, cwd = process.cwd()) {
|
|
128
|
+
await git(["commit", "-m", message], cwd);
|
|
129
|
+
return git(["rev-parse", "HEAD"], cwd);
|
|
130
|
+
}
|
|
131
|
+
// ─── Push ─────────────────────────────────────────────────────────────────────
|
|
132
|
+
export async function pushBranch(branchName, cwd = process.cwd()) {
|
|
133
|
+
await git(["push", "--set-upstream", "origin", branchName], cwd);
|
|
134
|
+
}
|
|
135
|
+
// ─── Repo inspection ──────────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* List all tracked files in the repo (respects .gitignore).
|
|
138
|
+
* Returns paths relative to `cwd`.
|
|
139
|
+
*/
|
|
140
|
+
export async function listTrackedFiles(cwd = process.cwd()) {
|
|
141
|
+
try {
|
|
142
|
+
const out = await git(["ls-files"], cwd);
|
|
143
|
+
return out
|
|
144
|
+
.split("\n")
|
|
145
|
+
.map((l) => l.trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get the unified diff of only staged changes (`git diff --cached`).
|
|
154
|
+
* Use this when the caller wants to commit only what's already in the index.
|
|
155
|
+
*/
|
|
156
|
+
export async function getStagedDiff(cwd = process.cwd()) {
|
|
157
|
+
try {
|
|
158
|
+
return await git(["diff", "--cached"], cwd);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get a compact stat summary of only staged changes (`git diff --cached --stat`).
|
|
166
|
+
*/
|
|
167
|
+
export async function getStagedDiffStat(cwd = process.cwd()) {
|
|
168
|
+
try {
|
|
169
|
+
return await git(["diff", "--cached", "--stat"], cwd);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get the unified diff of all uncommitted changes (staged + unstaged).
|
|
177
|
+
*/
|
|
178
|
+
export async function getWorkingDiff(cwd = process.cwd()) {
|
|
179
|
+
try {
|
|
180
|
+
const staged = await git(["diff", "--cached"], cwd);
|
|
181
|
+
const unstaged = await git(["diff"], cwd);
|
|
182
|
+
return [staged, unstaged].filter(Boolean).join("\n");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get a compact summary of staged changes (--stat format):
|
|
190
|
+
* lists every changed file with insertion/deletion counts.
|
|
191
|
+
* Always small regardless of diff size — used to give the AI the
|
|
192
|
+
* complete picture of what changed even when the full diff is truncated.
|
|
193
|
+
*/
|
|
194
|
+
export async function getWorkingDiffStat(cwd = process.cwd()) {
|
|
195
|
+
try {
|
|
196
|
+
const staged = await git(["diff", "--cached", "--stat"], cwd);
|
|
197
|
+
const unstaged = await git(["diff", "--stat"], cwd);
|
|
198
|
+
return [staged, unstaged].filter(Boolean).join("\n");
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Auto-detect the most likely base branch for the current feature branch.
|
|
206
|
+
*
|
|
207
|
+
* Strategy:
|
|
208
|
+
* 1. Check if the current branch has a configured upstream tracking branch.
|
|
209
|
+
* 2. Otherwise, try common default branch names (main, master, develop, dev).
|
|
210
|
+
* 3. For each candidate, count commits on HEAD that are NOT in that branch.
|
|
211
|
+
* The candidate with the fewest such commits is the likely origin.
|
|
212
|
+
*
|
|
213
|
+
* Falls back to "main" if nothing can be determined.
|
|
214
|
+
*/
|
|
215
|
+
export async function detectBaseBranch(cwd = process.cwd()) {
|
|
216
|
+
// Get current branch name upfront — used for all checks below
|
|
217
|
+
const current = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "");
|
|
218
|
+
// 1. Try upstream tracking branch — only useful if it points to a DIFFERENT
|
|
219
|
+
// branch (e.g. origin/main), not the branch's own remote tracking ref.
|
|
220
|
+
// e.g. "origin/gitx/test" → "gitx/test" == current → skip
|
|
221
|
+
// "origin/main" → "main" != current → use it
|
|
222
|
+
try {
|
|
223
|
+
const upstream = await git(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
|
|
224
|
+
const branch = upstream.replace(/^[^/]+\//, "").trim();
|
|
225
|
+
if (branch && branch !== current)
|
|
226
|
+
return branch;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// No upstream configured — fall through
|
|
230
|
+
}
|
|
231
|
+
// 2. Check remote HEAD (origin's default branch)
|
|
232
|
+
try {
|
|
233
|
+
const remoteHead = await git(["rev-parse", "--abbrev-ref", "origin/HEAD"], cwd);
|
|
234
|
+
const branch = remoteHead.replace(/^origin\//, "").trim();
|
|
235
|
+
if (branch && branch !== current)
|
|
236
|
+
return branch;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Not available — fall through
|
|
240
|
+
}
|
|
241
|
+
// 3. Count commits ahead of each common default branch name
|
|
242
|
+
const candidates = ["main", "master", "develop", "dev", "staging"];
|
|
243
|
+
const counts = [];
|
|
244
|
+
for (const candidate of candidates) {
|
|
245
|
+
if (candidate === current)
|
|
246
|
+
continue;
|
|
247
|
+
try {
|
|
248
|
+
// Count commits on HEAD not in candidate
|
|
249
|
+
const out = await git(["rev-list", "--count", `${candidate}..HEAD`], cwd);
|
|
250
|
+
counts.push({ branch: candidate, ahead: parseInt(out.trim(), 10) || 0 });
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Branch doesn't exist locally — skip
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (counts.length > 0) {
|
|
257
|
+
// Pick the branch with the fewest commits ahead (closest ancestor)
|
|
258
|
+
counts.sort((a, b) => a.ahead - b.ahead);
|
|
259
|
+
return counts[0].branch;
|
|
260
|
+
}
|
|
261
|
+
return "main";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the one-line commit log for commits on HEAD that are not in baseBranch.
|
|
265
|
+
* Used to give AI context about what this branch adds.
|
|
266
|
+
*/
|
|
267
|
+
export async function getBranchCommits(cwd = process.cwd(), baseBranch = "main") {
|
|
268
|
+
try {
|
|
269
|
+
const out = await git(["log", "--oneline", "--no-decorate", `${baseBranch}..HEAD`], cwd);
|
|
270
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get the unified diff of all changes between baseBranch and HEAD.
|
|
278
|
+
* This is what the PR reviewer would see — all additions across all commits.
|
|
279
|
+
*/
|
|
280
|
+
export async function getBranchDiff(cwd = process.cwd(), baseBranch = "main") {
|
|
281
|
+
try {
|
|
282
|
+
// Three-dot diff: all changes introduced by this branch vs. the merge base
|
|
283
|
+
return await git(["diff", `${baseBranch}...HEAD`], cwd);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get the --stat summary of all changes between baseBranch and HEAD.
|
|
291
|
+
* Lists every file changed with insertion/deletion counts.
|
|
292
|
+
* Used alongside a truncated diff so the AI sees the full file list even
|
|
293
|
+
* when the detailed patch is cut off.
|
|
294
|
+
*/
|
|
295
|
+
export async function getBranchStat(cwd = process.cwd(), baseBranch = "main") {
|
|
296
|
+
try {
|
|
297
|
+
return await git(["diff", "--stat", `${baseBranch}...HEAD`], cwd);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Read file content as a string. Returns empty string if file doesn't exist.
|
|
305
|
+
*/
|
|
306
|
+
export async function readRepoFile(relativePath, cwd = process.cwd()) {
|
|
307
|
+
try {
|
|
308
|
+
const abs = resolve(join(cwd, relativePath));
|
|
309
|
+
const { readFile } = await import("node:fs/promises");
|
|
310
|
+
return await readFile(abs, "utf-8");
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check whether a branch exists on the remote (origin).
|
|
318
|
+
* Uses git ls-remote which does not require a full fetch.
|
|
319
|
+
*/
|
|
320
|
+
export async function branchExistsOnRemote(branchName, cwd = process.cwd()) {
|
|
321
|
+
try {
|
|
322
|
+
const out = await git(["ls-remote", "--heads", "origin", `refs/heads/${branchName}`], cwd);
|
|
323
|
+
return out.trim().length > 0;
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Returns true if the working tree has uncommitted changes (staged or unstaged).
|
|
331
|
+
*/
|
|
332
|
+
export async function isWorkingTreeDirty(cwd = process.cwd()) {
|
|
333
|
+
try {
|
|
334
|
+
const out = await git(["status", "--porcelain"], cwd);
|
|
335
|
+
return out.trim().length > 0;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ─── Context helpers for `gitx ask` ──────────────────────────────────────────
|
|
342
|
+
/**
|
|
343
|
+
* Returns `git status --short` output for use in AI context.
|
|
344
|
+
* Returns an empty string when the working tree is clean or git fails.
|
|
345
|
+
*/
|
|
346
|
+
export async function getGitStatus(cwd = process.cwd()) {
|
|
347
|
+
try {
|
|
348
|
+
return await git(["status", "--short"], cwd);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return "";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returns the last `count` commits as one-line summaries (hash + subject).
|
|
356
|
+
* Defaults to the last 10 commits.
|
|
357
|
+
*/
|
|
358
|
+
export async function getRecentCommits(cwd = process.cwd(), count = 10) {
|
|
359
|
+
try {
|
|
360
|
+
const out = await git(["log", "--oneline", "--no-decorate", `-${count}`], cwd);
|
|
361
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Returns the list of git stashes as `stash@{0}: ...` strings.
|
|
369
|
+
* Returns an empty array when there are no stashes or git fails.
|
|
370
|
+
*/
|
|
371
|
+
export async function getStashList(cwd = process.cwd()) {
|
|
372
|
+
try {
|
|
373
|
+
const out = await git(["stash", "list"], cwd);
|
|
374
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=gitOps.js.map
|