@g-abhishek/gitx 0.1.2 → 0.1.5
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 +386 -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 +574 -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 -31
- 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 -69
- 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 +208 -7
- package/dist/cli/commands/pr/create.js.map +1 -1
- package/dist/cli/commands/pr/fixComments.d.ts +5 -2
- package/dist/cli/commands/pr/fixComments.d.ts.map +1 -1
- package/dist/cli/commands/pr/fixComments.js +5 -13
- 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 +6 -2
- 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 +24 -4
- 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/resolve.d.ts +3 -0
- package/dist/cli/commands/pr/resolve.d.ts.map +1 -0
- package/dist/cli/commands/pr/resolve.js +92 -0
- package/dist/cli/commands/pr/resolve.js.map +1 -0
- package/dist/cli/commands/pr/review.d.ts.map +1 -1
- package/dist/cli/commands/pr/review.js +121 -6
- 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 +98 -45
- package/dist/config/config.js.map +1 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +61 -6
- package/dist/config/schema.js.map +1 -1
- package/dist/core/context.d.ts +6 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js.map +1 -1
- package/dist/core/gitx.d.ts +43 -0
- package/dist/core/gitx.d.ts.map +1 -1
- package/dist/core/gitx.js +187 -20
- 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 +50 -7
- 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 +19 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +45 -8
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/gitOps.d.ts +125 -0
- package/dist/utils/gitOps.d.ts.map +1 -0
- package/dist/utils/gitOps.js +396 -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 +291 -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
|
@@ -20,24 +20,58 @@ export async function isInsideGitRepo(cwd = process.cwd()) {
|
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
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
|
+
*/
|
|
23
42
|
export function inferRepoSlugFromRemote(url) {
|
|
24
|
-
// Supports common GitHub/GitLab URL formats:
|
|
25
|
-
// - git@github.com:owner/repo.git
|
|
26
|
-
// - https://github.com/owner/repo(.git)
|
|
27
|
-
// - ssh://git@github.com/owner/repo(.git)
|
|
28
43
|
const cleaned = url.trim();
|
|
29
|
-
|
|
44
|
+
// ── GitHub / GitLab ──────────────────────────────────────────────────────
|
|
45
|
+
const ghGlPatterns = [
|
|
30
46
|
/^(?:git@)(?:github\.com|gitlab\.com):(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i,
|
|
31
47
|
/^(?:https?:\/\/)(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?(?:\/)?$/i,
|
|
32
|
-
/^(?:ssh:\/\/)(?:git@)(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i
|
|
48
|
+
/^(?:ssh:\/\/)(?:git@)?(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i,
|
|
33
49
|
];
|
|
34
|
-
for (const re of
|
|
50
|
+
for (const re of ghGlPatterns) {
|
|
35
51
|
const match = re.exec(cleaned);
|
|
36
52
|
const owner = match?.groups?.["owner"];
|
|
37
53
|
const repo = match?.groups?.["repo"];
|
|
38
54
|
if (owner && repo)
|
|
39
55
|
return `${owner}/${repo}`;
|
|
40
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]}`;
|
|
41
75
|
return undefined;
|
|
42
76
|
}
|
|
43
77
|
export async function resolveRepoSlugFromCwd(cwd = process.cwd()) {
|
|
@@ -52,8 +86,11 @@ export function detectProviderFromRemote(url) {
|
|
|
52
86
|
return "github";
|
|
53
87
|
if (cleaned.includes("gitlab.com"))
|
|
54
88
|
return "gitlab";
|
|
55
|
-
if (cleaned.includes("dev.azure.com") ||
|
|
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
|
+
) {
|
|
56
92
|
return "azure";
|
|
93
|
+
}
|
|
57
94
|
return undefined;
|
|
58
95
|
}
|
|
59
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;AAGtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC7D,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,mBAAmB,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACjG,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACxC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IACvD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QAC/F,OAAO,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,GAAW;IACjD,
|
|
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,125 @@
|
|
|
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. Upstream tracking branch set via `git push -u` — strongest signal.
|
|
66
|
+
* Ignored if it points to the branch's own remote ref (origin/current).
|
|
67
|
+
* 2. Scan ALL remote tracking branches and count how many commits HEAD has
|
|
68
|
+
* that each candidate doesn't (`git rev-list --count <ref>..HEAD`).
|
|
69
|
+
* The branch with the FEWEST such commits is the closest ancestor —
|
|
70
|
+
* i.e. the branch this one was forked from.
|
|
71
|
+
*
|
|
72
|
+
* Example: main → feature1 (3 commits) → feature2 (2 commits)
|
|
73
|
+
* - origin/main..HEAD = 5 (feature1's 3 + feature2's 2)
|
|
74
|
+
* - origin/feature1..HEAD = 2 (just feature2's commits)
|
|
75
|
+
* → feature1 wins ✓
|
|
76
|
+
*
|
|
77
|
+
* Falls back to "main" if nothing can be determined.
|
|
78
|
+
*/
|
|
79
|
+
export declare function detectBaseBranch(cwd?: string): Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Get the one-line commit log for commits on HEAD that are not in baseBranch.
|
|
82
|
+
* Used to give AI context about what this branch adds.
|
|
83
|
+
*/
|
|
84
|
+
export declare function getBranchCommits(cwd?: string, baseBranch?: string): Promise<string[]>;
|
|
85
|
+
/**
|
|
86
|
+
* Get the unified diff of all changes between baseBranch and HEAD.
|
|
87
|
+
* This is what the PR reviewer would see — all additions across all commits.
|
|
88
|
+
*/
|
|
89
|
+
export declare function getBranchDiff(cwd?: string, baseBranch?: string): Promise<string>;
|
|
90
|
+
/**
|
|
91
|
+
* Get the --stat summary of all changes between baseBranch and HEAD.
|
|
92
|
+
* Lists every file changed with insertion/deletion counts.
|
|
93
|
+
* Used alongside a truncated diff so the AI sees the full file list even
|
|
94
|
+
* when the detailed patch is cut off.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getBranchStat(cwd?: string, baseBranch?: string): Promise<string>;
|
|
97
|
+
/**
|
|
98
|
+
* Read file content as a string. Returns empty string if file doesn't exist.
|
|
99
|
+
*/
|
|
100
|
+
export declare function readRepoFile(relativePath: string, cwd?: string): Promise<string | undefined>;
|
|
101
|
+
/**
|
|
102
|
+
* Check whether a branch exists on the remote (origin).
|
|
103
|
+
* Uses git ls-remote which does not require a full fetch.
|
|
104
|
+
*/
|
|
105
|
+
export declare function branchExistsOnRemote(branchName: string, cwd?: string): Promise<boolean>;
|
|
106
|
+
/**
|
|
107
|
+
* Returns true if the working tree has uncommitted changes (staged or unstaged).
|
|
108
|
+
*/
|
|
109
|
+
export declare function isWorkingTreeDirty(cwd?: string): Promise<boolean>;
|
|
110
|
+
/**
|
|
111
|
+
* Returns `git status --short` output for use in AI context.
|
|
112
|
+
* Returns an empty string when the working tree is clean or git fails.
|
|
113
|
+
*/
|
|
114
|
+
export declare function getGitStatus(cwd?: string): Promise<string>;
|
|
115
|
+
/**
|
|
116
|
+
* Returns the last `count` commits as one-line summaries (hash + subject).
|
|
117
|
+
* Defaults to the last 10 commits.
|
|
118
|
+
*/
|
|
119
|
+
export declare function getRecentCommits(cwd?: string, count?: number): Promise<string[]>;
|
|
120
|
+
/**
|
|
121
|
+
* Returns the list of git stashes as `stash@{0}: ...` strings.
|
|
122
|
+
* Returns an empty array when there are no stashes or git fails.
|
|
123
|
+
*/
|
|
124
|
+
export declare function getStashList(cwd?: string): Promise<string[]>;
|
|
125
|
+
//# 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,396 @@
|
|
|
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. Upstream tracking branch set via `git push -u` — strongest signal.
|
|
209
|
+
* Ignored if it points to the branch's own remote ref (origin/current).
|
|
210
|
+
* 2. Scan ALL remote tracking branches and count how many commits HEAD has
|
|
211
|
+
* that each candidate doesn't (`git rev-list --count <ref>..HEAD`).
|
|
212
|
+
* The branch with the FEWEST such commits is the closest ancestor —
|
|
213
|
+
* i.e. the branch this one was forked from.
|
|
214
|
+
*
|
|
215
|
+
* Example: main → feature1 (3 commits) → feature2 (2 commits)
|
|
216
|
+
* - origin/main..HEAD = 5 (feature1's 3 + feature2's 2)
|
|
217
|
+
* - origin/feature1..HEAD = 2 (just feature2's commits)
|
|
218
|
+
* → feature1 wins ✓
|
|
219
|
+
*
|
|
220
|
+
* Falls back to "main" if nothing can be determined.
|
|
221
|
+
*/
|
|
222
|
+
export async function detectBaseBranch(cwd = process.cwd()) {
|
|
223
|
+
const current = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "");
|
|
224
|
+
// 1. Explicit upstream tracking branch — only trust it if it differs from
|
|
225
|
+
// the current branch (guards against "origin/feature2" → "feature2").
|
|
226
|
+
try {
|
|
227
|
+
const upstream = await git(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
|
|
228
|
+
const branch = upstream.replace(/^[^/]+\//, "").trim();
|
|
229
|
+
if (branch && branch !== current)
|
|
230
|
+
return branch;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// No upstream set — fall through
|
|
234
|
+
}
|
|
235
|
+
// 2. Scan all remote tracking branches (origin/*), compute how many commits
|
|
236
|
+
// HEAD has that each remote ref doesn't. Closest ancestor wins.
|
|
237
|
+
try {
|
|
238
|
+
const remoteOut = await git(["branch", "-r", "--format=%(refname:short)"], cwd);
|
|
239
|
+
const remoteRefs = remoteOut
|
|
240
|
+
.split("\n")
|
|
241
|
+
.map((b) => b.trim())
|
|
242
|
+
.filter((b) => b && !b.endsWith("/HEAD")); // skip "origin/HEAD" pointer
|
|
243
|
+
if (remoteRefs.length === 0)
|
|
244
|
+
return "main";
|
|
245
|
+
// Count commits ahead of every remote ref in parallel for speed
|
|
246
|
+
const results = await Promise.all(remoteRefs.map(async (ref) => {
|
|
247
|
+
const name = ref.replace(/^[^/]+\//, ""); // "origin/feature1" → "feature1"
|
|
248
|
+
if (name === current)
|
|
249
|
+
return null; // skip own remote tracking ref
|
|
250
|
+
try {
|
|
251
|
+
const out = await git(["rev-list", "--count", `${ref}..HEAD`], cwd);
|
|
252
|
+
return { branch: name, ahead: parseInt(out.trim(), 10) || 0 };
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}));
|
|
258
|
+
const counts = results.filter((r) => r !== null);
|
|
259
|
+
if (counts.length === 0)
|
|
260
|
+
return "main";
|
|
261
|
+
// Sort by fewest commits ahead — that's the closest ancestor.
|
|
262
|
+
// Tiebreak: prefer default branch names so "main" beats an equally-close
|
|
263
|
+
// unrelated branch that happens to have 0 commits.
|
|
264
|
+
const defaults = new Set(["main", "master", "develop", "dev", "staging"]);
|
|
265
|
+
counts.sort((a, b) => {
|
|
266
|
+
if (a.ahead !== b.ahead)
|
|
267
|
+
return a.ahead - b.ahead;
|
|
268
|
+
// Same distance — prefer default branches
|
|
269
|
+
const aDefault = defaults.has(a.branch) ? 0 : 1;
|
|
270
|
+
const bDefault = defaults.has(b.branch) ? 0 : 1;
|
|
271
|
+
return aDefault - bDefault;
|
|
272
|
+
});
|
|
273
|
+
return counts[0].branch;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return "main";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get the one-line commit log for commits on HEAD that are not in baseBranch.
|
|
281
|
+
* Used to give AI context about what this branch adds.
|
|
282
|
+
*/
|
|
283
|
+
export async function getBranchCommits(cwd = process.cwd(), baseBranch = "main") {
|
|
284
|
+
try {
|
|
285
|
+
const out = await git(["log", "--oneline", "--no-decorate", `${baseBranch}..HEAD`], cwd);
|
|
286
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get the unified diff of all changes between baseBranch and HEAD.
|
|
294
|
+
* This is what the PR reviewer would see — all additions across all commits.
|
|
295
|
+
*/
|
|
296
|
+
export async function getBranchDiff(cwd = process.cwd(), baseBranch = "main") {
|
|
297
|
+
try {
|
|
298
|
+
// Three-dot diff: all changes introduced by this branch vs. the merge base
|
|
299
|
+
return await git(["diff", `${baseBranch}...HEAD`], cwd);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return "";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get the --stat summary of all changes between baseBranch and HEAD.
|
|
307
|
+
* Lists every file changed with insertion/deletion counts.
|
|
308
|
+
* Used alongside a truncated diff so the AI sees the full file list even
|
|
309
|
+
* when the detailed patch is cut off.
|
|
310
|
+
*/
|
|
311
|
+
export async function getBranchStat(cwd = process.cwd(), baseBranch = "main") {
|
|
312
|
+
try {
|
|
313
|
+
return await git(["diff", "--stat", `${baseBranch}...HEAD`], cwd);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return "";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Read file content as a string. Returns empty string if file doesn't exist.
|
|
321
|
+
*/
|
|
322
|
+
export async function readRepoFile(relativePath, cwd = process.cwd()) {
|
|
323
|
+
try {
|
|
324
|
+
const abs = resolve(join(cwd, relativePath));
|
|
325
|
+
const { readFile } = await import("node:fs/promises");
|
|
326
|
+
return await readFile(abs, "utf-8");
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Check whether a branch exists on the remote (origin).
|
|
334
|
+
* Uses git ls-remote which does not require a full fetch.
|
|
335
|
+
*/
|
|
336
|
+
export async function branchExistsOnRemote(branchName, cwd = process.cwd()) {
|
|
337
|
+
try {
|
|
338
|
+
const out = await git(["ls-remote", "--heads", "origin", `refs/heads/${branchName}`], cwd);
|
|
339
|
+
return out.trim().length > 0;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Returns true if the working tree has uncommitted changes (staged or unstaged).
|
|
347
|
+
*/
|
|
348
|
+
export async function isWorkingTreeDirty(cwd = process.cwd()) {
|
|
349
|
+
try {
|
|
350
|
+
const out = await git(["status", "--porcelain"], cwd);
|
|
351
|
+
return out.trim().length > 0;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// ─── Context helpers for `gitx ask` ──────────────────────────────────────────
|
|
358
|
+
/**
|
|
359
|
+
* Returns `git status --short` output for use in AI context.
|
|
360
|
+
* Returns an empty string when the working tree is clean or git fails.
|
|
361
|
+
*/
|
|
362
|
+
export async function getGitStatus(cwd = process.cwd()) {
|
|
363
|
+
try {
|
|
364
|
+
return await git(["status", "--short"], cwd);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return "";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Returns the last `count` commits as one-line summaries (hash + subject).
|
|
372
|
+
* Defaults to the last 10 commits.
|
|
373
|
+
*/
|
|
374
|
+
export async function getRecentCommits(cwd = process.cwd(), count = 10) {
|
|
375
|
+
try {
|
|
376
|
+
const out = await git(["log", "--oneline", "--no-decorate", `-${count}`], cwd);
|
|
377
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Returns the list of git stashes as `stash@{0}: ...` strings.
|
|
385
|
+
* Returns an empty array when there are no stashes or git fails.
|
|
386
|
+
*/
|
|
387
|
+
export async function getStashList(cwd = process.cwd()) {
|
|
388
|
+
try {
|
|
389
|
+
const out = await git(["stash", "list"], cwd);
|
|
390
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
//# sourceMappingURL=gitOps.js.map
|