@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitx push
|
|
3
|
+
*
|
|
4
|
+
* The single-command workflow: stage everything → AI-generate a conventional
|
|
5
|
+
* commit message → commit → push to origin. No flags needed.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* gitx push # full auto: stage + AI commit + push
|
|
9
|
+
* gitx push --staged # commit only already-staged changes, leave the rest untouched
|
|
10
|
+
* gitx push -m "fix: typo" # use a custom message, skip AI
|
|
11
|
+
* gitx push --dry-run # preview message without committing or pushing
|
|
12
|
+
* gitx push --branch feat # push to a specific branch instead of current
|
|
13
|
+
*/
|
|
14
|
+
import inquirer from "inquirer";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import { logger } from "../../logger/logger.js";
|
|
17
|
+
import { Gitx } from "../../core/gitx.js";
|
|
18
|
+
import { stageAll, hasStagedChanges, isWorkingTreeDirty, getWorkingDiff, getWorkingDiffStat, getStagedDiff, getStagedDiffStat, commitChanges, pushBranch, getCurrentBranch, } from "../../utils/gitOps.js";
|
|
19
|
+
import { isInsideGitRepo } from "../../utils/git.js";
|
|
20
|
+
import { GitxError } from "../../utils/errors.js";
|
|
21
|
+
import { withLockRetry } from "../../utils/lockFile.js";
|
|
22
|
+
export function registerPushCommand(program) {
|
|
23
|
+
program
|
|
24
|
+
.command("push")
|
|
25
|
+
.description("🚀 Stage, AI-commit, and push to origin in one step")
|
|
26
|
+
.option("-m, --message <msg>", "Use a custom commit message (skips AI generation)")
|
|
27
|
+
.option("--staged", "Commit only already-staged changes — leave unstaged files untouched")
|
|
28
|
+
.option("--dry-run", "Preview the commit message without committing or pushing")
|
|
29
|
+
.option("-b, --branch <name>", "Push to this branch instead of the current branch")
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
// ── Guards ─────────────────────────────────────────────────────────────
|
|
33
|
+
if (!(await isInsideGitRepo(cwd))) {
|
|
34
|
+
throw new GitxError("Not inside a git repository. cd into your project folder first.", { exitCode: 2 });
|
|
35
|
+
}
|
|
36
|
+
const dirty = await isWorkingTreeDirty(cwd);
|
|
37
|
+
if (!dirty) {
|
|
38
|
+
logger.info("✨ Nothing to commit — working tree is clean.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// ── Stage changes ──────────────────────────────────────────────────────
|
|
42
|
+
if (opts.staged) {
|
|
43
|
+
// --staged: only commit what's already in the index, leave the rest alone
|
|
44
|
+
const alreadyStaged = await hasStagedChanges(cwd);
|
|
45
|
+
if (!alreadyStaged) {
|
|
46
|
+
logger.warn("⚠️ No staged changes found. Stage files first with `git add <files>` then retry.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
logger.info("📦 Using already-staged changes only (--staged).");
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Default: stage everything
|
|
53
|
+
const stageSpinner = ora("Staging all changes…").start();
|
|
54
|
+
await withLockRetry(() => stageAll(cwd), cwd);
|
|
55
|
+
stageSpinner.succeed("All changes staged.");
|
|
56
|
+
const staged = await hasStagedChanges(cwd);
|
|
57
|
+
if (!staged) {
|
|
58
|
+
logger.warn("No staged changes found after staging. Aborting.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ── Get diff for AI ────────────────────────────────────────────────────
|
|
63
|
+
// stat = compact file list (always complete, never truncated)
|
|
64
|
+
// diff = full patch (may be large — we truncate later in the AI call)
|
|
65
|
+
const [stat, diff] = await Promise.all([
|
|
66
|
+
opts.staged ? getStagedDiffStat(cwd) : getWorkingDiffStat(cwd),
|
|
67
|
+
opts.staged ? getStagedDiff(cwd) : getWorkingDiff(cwd),
|
|
68
|
+
]);
|
|
69
|
+
// ── Generate or use custom commit message ──────────────────────────────
|
|
70
|
+
let commitMsg;
|
|
71
|
+
if (opts.message) {
|
|
72
|
+
commitMsg = opts.message;
|
|
73
|
+
logger.info(`📝 Using provided message: ${commitMsg}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const gitx = await Gitx.fromCwd(cwd);
|
|
77
|
+
if (!await Gitx.isAiAvailable(gitx.config)) {
|
|
78
|
+
logger.warn("⚠️ No AI provider configured. Run `gitx config` to set one up.");
|
|
79
|
+
logger.warn(" Falling back to manual commit message entry.\n");
|
|
80
|
+
const { manualMsg } = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: "input",
|
|
83
|
+
name: "manualMsg",
|
|
84
|
+
message: "Commit message:",
|
|
85
|
+
validate: (v) => v.trim().length > 0 || "Message cannot be empty",
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
commitMsg = manualMsg.trim();
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const aiSpinner = ora("🤖 Generating commit message…").start();
|
|
92
|
+
try {
|
|
93
|
+
// Build AI input: complete file summary first, then detailed patch.
|
|
94
|
+
// The stat ensures the AI sees every changed file even when the
|
|
95
|
+
// full diff is truncated by the 12 000 char safety limit.
|
|
96
|
+
const aiInput = stat
|
|
97
|
+
? `=== Changed files (complete list) ===\n${stat}\n\n=== Detailed diff ===\n${diff}`
|
|
98
|
+
: diff;
|
|
99
|
+
const result = await gitx.ai.generateCommitMessage(aiInput);
|
|
100
|
+
commitMsg = result.body
|
|
101
|
+
? `${result.subject}\n\n${result.body}`
|
|
102
|
+
: result.subject;
|
|
103
|
+
aiSpinner.succeed("Commit message generated.");
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
aiSpinner.fail("AI generation failed.");
|
|
107
|
+
logger.warn(` ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
logger.warn(" Falling back to manual entry.\n");
|
|
109
|
+
const { manualMsg } = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
type: "input",
|
|
112
|
+
name: "manualMsg",
|
|
113
|
+
message: "Commit message:",
|
|
114
|
+
validate: (v) => v.trim().length > 0 || "Message cannot be empty",
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
commitMsg = manualMsg.trim();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── Preview + confirm ──────────────────────────────────────────────────
|
|
122
|
+
logger.info("\n📋 Commit message preview:\n");
|
|
123
|
+
logger.info("─".repeat(60));
|
|
124
|
+
logger.info(commitMsg);
|
|
125
|
+
logger.info("─".repeat(60));
|
|
126
|
+
if (opts.dryRun) {
|
|
127
|
+
logger.info("\n🔍 Dry run — nothing committed or pushed.");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const { confirmed } = await inquirer.prompt([
|
|
131
|
+
{
|
|
132
|
+
type: "confirm",
|
|
133
|
+
name: "confirmed",
|
|
134
|
+
message: "Commit and push with this message?",
|
|
135
|
+
default: true,
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
if (!confirmed) {
|
|
139
|
+
// Give the user a chance to edit before we proceed
|
|
140
|
+
const { editedMsg } = await inquirer.prompt([
|
|
141
|
+
{
|
|
142
|
+
type: "editor",
|
|
143
|
+
name: "editedMsg",
|
|
144
|
+
message: "Edit commit message:",
|
|
145
|
+
default: commitMsg,
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
commitMsg = editedMsg.trim();
|
|
149
|
+
if (!commitMsg) {
|
|
150
|
+
logger.warn("Empty message — aborted.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ── Commit ─────────────────────────────────────────────────────────────
|
|
155
|
+
const commitSpinner = ora("Committing…").start();
|
|
156
|
+
await withLockRetry(() => commitChanges(commitMsg, cwd), cwd);
|
|
157
|
+
commitSpinner.succeed("Committed ✓");
|
|
158
|
+
// ── Push ───────────────────────────────────────────────────────────────
|
|
159
|
+
const branch = opts.branch ?? (await getCurrentBranch(cwd));
|
|
160
|
+
const pushSpinner = ora(`Pushing ${branch} to origin…`).start();
|
|
161
|
+
await pushBranch(branch, cwd);
|
|
162
|
+
pushSpinner.succeed(`Pushed to origin/${branch} ✓`);
|
|
163
|
+
logger.success(`\n✅ Done! Changes are live on origin/${branch}.`);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../../src/cli/commands/push.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gitx push\n *\n * The single-command workflow: stage everything → AI-generate a conventional\n * commit message → commit → push to origin. No flags needed.\n *\n * Usage:\n * gitx push # full auto: stage + AI commit + push\n * gitx push --staged # commit only already-staged changes, leave the rest untouched\n * gitx push -m \"fix: typo\" # use a custom message, skip AI\n * gitx push --dry-run # preview message without committing or pushing\n * gitx push --branch feat # push to a specific branch instead of current\n */\n\nimport type { Command } from \"commander\";\nimport inquirer from \"inquirer\";\nimport ora from \"ora\";\nimport { logger } from \"../../logger/logger.js\";\nimport { Gitx } from \"../../core/gitx.js\";\nimport {\n stageAll,\n hasStagedChanges,\n isWorkingTreeDirty,\n getWorkingDiff,\n getWorkingDiffStat,\n getStagedDiff,\n getStagedDiffStat,\n commitChanges,\n pushBranch,\n getCurrentBranch,\n} from \"../../utils/gitOps.js\";\nimport { isInsideGitRepo } from \"../../utils/git.js\";\nimport { GitxError } from \"../../utils/errors.js\";\nimport { withLockRetry } from \"../../utils/lockFile.js\";\n\nexport function registerPushCommand(program: Command): void {\n program\n .command(\"push\")\n .description(\"🚀 Stage, AI-commit, and push to origin in one step\")\n .option(\"-m, --message <msg>\", \"Use a custom commit message (skips AI generation)\")\n .option(\"--staged\", \"Commit only already-staged changes — leave unstaged files untouched\")\n .option(\"--dry-run\", \"Preview the commit message without committing or pushing\")\n .option(\"-b, --branch <name>\", \"Push to this branch instead of the current branch\")\n .action(async (opts: {\n message?: string;\n staged?: boolean;\n dryRun?: boolean;\n branch?: string;\n }) => {\n const cwd = process.cwd();\n\n // ── Guards ─────────────────────────────────────────────────────────────\n if (!(await isInsideGitRepo(cwd))) {\n throw new GitxError(\"Not inside a git repository. cd into your project folder first.\", { exitCode: 2 });\n }\n\n const dirty = await isWorkingTreeDirty(cwd);\n if (!dirty) {\n logger.info(\"✨ Nothing to commit — working tree is clean.\");\n return;\n }\n\n // ── Stage changes ──────────────────────────────────────────────────────\n if (opts.staged) {\n // --staged: only commit what's already in the index, leave the rest alone\n const alreadyStaged = await hasStagedChanges(cwd);\n if (!alreadyStaged) {\n logger.warn(\"⚠️ No staged changes found. Stage files first with `git add <files>` then retry.\");\n return;\n }\n logger.info(\"📦 Using already-staged changes only (--staged).\");\n } else {\n // Default: stage everything\n const stageSpinner = ora(\"Staging all changes…\").start();\n await withLockRetry(() => stageAll(cwd), cwd);\n stageSpinner.succeed(\"All changes staged.\");\n\n const staged = await hasStagedChanges(cwd);\n if (!staged) {\n logger.warn(\"No staged changes found after staging. Aborting.\");\n return;\n }\n }\n\n // ── Get diff for AI ────────────────────────────────────────────────────\n // stat = compact file list (always complete, never truncated)\n // diff = full patch (may be large — we truncate later in the AI call)\n const [stat, diff] = await Promise.all([\n opts.staged ? getStagedDiffStat(cwd) : getWorkingDiffStat(cwd),\n opts.staged ? getStagedDiff(cwd) : getWorkingDiff(cwd),\n ]);\n\n // ── Generate or use custom commit message ──────────────────────────────\n let commitMsg: string;\n\n if (opts.message) {\n commitMsg = opts.message;\n logger.info(`📝 Using provided message: ${commitMsg}`);\n } else {\n const gitx = await Gitx.fromCwd(cwd);\n\n if (!await Gitx.isAiAvailable(gitx.config)) {\n logger.warn(\"⚠️ No AI provider configured. Run `gitx config` to set one up.\");\n logger.warn(\" Falling back to manual commit message entry.\\n\");\n\n const { manualMsg } = await inquirer.prompt<{ manualMsg: string }>([\n {\n type: \"input\",\n name: \"manualMsg\",\n message: \"Commit message:\",\n validate: (v: string) => v.trim().length > 0 || \"Message cannot be empty\",\n },\n ]);\n commitMsg = manualMsg.trim();\n } else {\n const aiSpinner = ora(\"🤖 Generating commit message…\").start();\n try {\n // Build AI input: complete file summary first, then detailed patch.\n // The stat ensures the AI sees every changed file even when the\n // full diff is truncated by the 12 000 char safety limit.\n const aiInput = stat\n ? `=== Changed files (complete list) ===\\n${stat}\\n\\n=== Detailed diff ===\\n${diff}`\n : diff;\n const result = await gitx.ai.generateCommitMessage(aiInput);\n\n commitMsg = result.body\n ? `${result.subject}\\n\\n${result.body}`\n : result.subject;\n\n aiSpinner.succeed(\"Commit message generated.\");\n } catch (err) {\n aiSpinner.fail(\"AI generation failed.\");\n logger.warn(` ${err instanceof Error ? err.message : String(err)}`);\n logger.warn(\" Falling back to manual entry.\\n\");\n\n const { manualMsg } = await inquirer.prompt<{ manualMsg: string }>([\n {\n type: \"input\",\n name: \"manualMsg\",\n message: \"Commit message:\",\n validate: (v: string) => v.trim().length > 0 || \"Message cannot be empty\",\n },\n ]);\n commitMsg = manualMsg.trim();\n }\n }\n }\n\n // ── Preview + confirm ──────────────────────────────────────────────────\n logger.info(\"\\n📋 Commit message preview:\\n\");\n logger.info(\"─\".repeat(60));\n logger.info(commitMsg);\n logger.info(\"─\".repeat(60));\n\n if (opts.dryRun) {\n logger.info(\"\\n🔍 Dry run — nothing committed or pushed.\");\n return;\n }\n\n const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([\n {\n type: \"confirm\",\n name: \"confirmed\",\n message: \"Commit and push with this message?\",\n default: true,\n },\n ]);\n\n if (!confirmed) {\n // Give the user a chance to edit before we proceed\n const { editedMsg } = await inquirer.prompt<{ editedMsg: string }>([\n {\n type: \"editor\",\n name: \"editedMsg\",\n message: \"Edit commit message:\",\n default: commitMsg,\n },\n ]);\n commitMsg = editedMsg.trim();\n if (!commitMsg) {\n logger.warn(\"Empty message — aborted.\");\n return;\n }\n }\n\n // ── Commit ─────────────────────────────────────────────────────────────\n const commitSpinner = ora(\"Committing…\").start();\n await withLockRetry(() => commitChanges(commitMsg, cwd), cwd);\n commitSpinner.succeed(\"Committed ✓\");\n\n // ── Push ───────────────────────────────────────────────────────────────\n const branch = opts.branch ?? (await getCurrentBranch(cwd));\n const pushSpinner = ora(`Pushing ${branch} to origin…`).start();\n await pushBranch(branch, cwd);\n pushSpinner.succeed(`Pushed to origin/${branch} ✓`);\n\n logger.success(`\\n✅ Done! Changes are live on origin/${branch}.`);\n });\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitx sync
|
|
3
|
+
*
|
|
4
|
+
* Bring the current branch up to date with its base branch so a PR can be
|
|
5
|
+
* merged cleanly. Uses rebase by default (keeps a linear history).
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Detect base branch (same logic as `gitx pr create`)
|
|
9
|
+
* 2. git fetch origin
|
|
10
|
+
* 3. git rebase origin/<base> (or --merge: git merge origin/<base>)
|
|
11
|
+
* 4a. No conflicts → git push --force-with-lease → "ready to merge"
|
|
12
|
+
* 4b. Conflicts → list conflicting files, instruct user how to resolve,
|
|
13
|
+
* then run `gitx sync --continue` to finish
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* gitx sync # rebase onto auto-detected base
|
|
17
|
+
* gitx sync --base main # rebase onto a specific base
|
|
18
|
+
* gitx sync --strategy merge # merge base into branch instead of rebase
|
|
19
|
+
* gitx sync --continue # after manually resolving conflicts
|
|
20
|
+
* gitx sync --abort # abort an in-progress rebase/merge
|
|
21
|
+
*/
|
|
22
|
+
import type { Command } from "commander";
|
|
23
|
+
export declare function registerSyncCommand(program: Command): void;
|
|
24
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqEzC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0S1D"}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitx sync
|
|
3
|
+
*
|
|
4
|
+
* Bring the current branch up to date with its base branch so a PR can be
|
|
5
|
+
* merged cleanly. Uses rebase by default (keeps a linear history).
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Detect base branch (same logic as `gitx pr create`)
|
|
9
|
+
* 2. git fetch origin
|
|
10
|
+
* 3. git rebase origin/<base> (or --merge: git merge origin/<base>)
|
|
11
|
+
* 4a. No conflicts → git push --force-with-lease → "ready to merge"
|
|
12
|
+
* 4b. Conflicts → list conflicting files, instruct user how to resolve,
|
|
13
|
+
* then run `gitx sync --continue` to finish
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* gitx sync # rebase onto auto-detected base
|
|
17
|
+
* gitx sync --base main # rebase onto a specific base
|
|
18
|
+
* gitx sync --strategy merge # merge base into branch instead of rebase
|
|
19
|
+
* gitx sync --continue # after manually resolving conflicts
|
|
20
|
+
* gitx sync --abort # abort an in-progress rebase/merge
|
|
21
|
+
*/
|
|
22
|
+
import ora from "ora";
|
|
23
|
+
import { execFile } from "node:child_process";
|
|
24
|
+
import { promisify } from "node:util";
|
|
25
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
26
|
+
import { resolve as resolvePath } from "node:path";
|
|
27
|
+
import { confirm, select } from "@inquirer/prompts";
|
|
28
|
+
import { logger } from "../../logger/logger.js";
|
|
29
|
+
import { getCurrentBranch, detectBaseBranch } from "../../utils/gitOps.js";
|
|
30
|
+
import { isInsideGitRepo } from "../../utils/git.js";
|
|
31
|
+
import { GitxError } from "../../utils/errors.js";
|
|
32
|
+
import { Gitx } from "../../core/gitx.js";
|
|
33
|
+
import { createProvider } from "../../providers/factory.js";
|
|
34
|
+
import { runAddressWorkflow, filterUnresolvedInlineComments } from "../../workflows/prAddress.js";
|
|
35
|
+
const execFileAsync = promisify(execFile);
|
|
36
|
+
async function git(args, cwd) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await execFileAsync("git", args, { cwd });
|
|
39
|
+
return { stdout: result.stdout.trim(), stderr: "" };
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const e = err;
|
|
43
|
+
return {
|
|
44
|
+
stdout: e.stdout?.trim() ?? "",
|
|
45
|
+
stderr: (e.stderr ?? e.message ?? String(err)).trim(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Returns list of files that currently have conflict markers. */
|
|
50
|
+
async function getConflictingFiles(cwd) {
|
|
51
|
+
const { stdout } = await git(["diff", "--name-only", "--diff-filter=U"], cwd);
|
|
52
|
+
return stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
/** Check whether a rebase or merge is currently in progress. */
|
|
55
|
+
async function getInProgressOperation(cwd) {
|
|
56
|
+
const { stdout: gitDir } = await git(["rev-parse", "--git-dir"], cwd);
|
|
57
|
+
const base = gitDir || ".git";
|
|
58
|
+
const { stdout: rebaseDir } = await git(["rev-parse", "--git-path", "rebase-merge"], cwd);
|
|
59
|
+
const { stdout: rebaseApply } = await git(["rev-parse", "--git-path", "rebase-apply"], cwd);
|
|
60
|
+
const { stdout: mergeHead } = await git(["rev-parse", "--git-path", "MERGE_HEAD"], cwd);
|
|
61
|
+
// Check if the paths actually exist on disk
|
|
62
|
+
const { existsSync } = await import("node:fs");
|
|
63
|
+
if (existsSync(rebaseDir) || existsSync(rebaseApply))
|
|
64
|
+
return "rebase";
|
|
65
|
+
if (existsSync(mergeHead) || existsSync(`${base}/MERGE_HEAD`))
|
|
66
|
+
return "merge";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
export function registerSyncCommand(program) {
|
|
70
|
+
program
|
|
71
|
+
.command("sync")
|
|
72
|
+
.description("🔄 Sync current branch with its base branch (merge by default, or --strategy rebase)")
|
|
73
|
+
.option("--base <branch>", "Base branch to sync with (auto-detected if omitted)")
|
|
74
|
+
.option("--strategy <strategy>", "Sync strategy: merge (default) | rebase", "merge")
|
|
75
|
+
.option("--continue", "Continue after manually resolving conflicts")
|
|
76
|
+
.option("--abort", "Abort an in-progress rebase or merge")
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
const cwd = process.cwd();
|
|
79
|
+
if (!(await isInsideGitRepo(cwd))) {
|
|
80
|
+
throw new GitxError("Not inside a git repository. cd into your project folder first.", { exitCode: 2 });
|
|
81
|
+
}
|
|
82
|
+
const strategy = opts.strategy === "rebase" ? "rebase" : "merge";
|
|
83
|
+
// ── Handle --abort ─────────────────────────────────────────────────────
|
|
84
|
+
if (opts.abort) {
|
|
85
|
+
const op = await getInProgressOperation(cwd);
|
|
86
|
+
if (!op) {
|
|
87
|
+
logger.info("No rebase or merge in progress.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const abortSpinner = ora(`Aborting ${op}…`).start();
|
|
91
|
+
const { stderr } = await git([op, "--abort"], cwd);
|
|
92
|
+
if (stderr) {
|
|
93
|
+
abortSpinner.fail(`Abort failed: ${stderr}`);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
abortSpinner.succeed(`${op.charAt(0).toUpperCase() + op.slice(1)} aborted. Branch restored to its previous state.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// ── Handle --continue ──────────────────────────────────────────────────
|
|
101
|
+
if (opts.continue) {
|
|
102
|
+
const op = await getInProgressOperation(cwd);
|
|
103
|
+
if (!op) {
|
|
104
|
+
logger.info("No rebase or merge in progress — nothing to continue.");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Check if all conflicts are resolved
|
|
108
|
+
const conflicts = await getConflictingFiles(cwd);
|
|
109
|
+
if (conflicts.length > 0) {
|
|
110
|
+
logger.error(`\n❌ There are still unresolved conflicts:\n`);
|
|
111
|
+
for (const f of conflicts)
|
|
112
|
+
logger.info(` • ${f}`);
|
|
113
|
+
logger.info(`\n Fix the conflicts above, then:\n`);
|
|
114
|
+
logger.info(` git add <file> # mark each file as resolved`);
|
|
115
|
+
logger.info(` gitx sync --continue # resume\n`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Stage all resolved files and continue
|
|
120
|
+
await git(["add", "-A"], cwd);
|
|
121
|
+
const continueSpinner = ora(`Continuing ${op}…`).start();
|
|
122
|
+
const env = { ...process.env, GIT_EDITOR: "true" }; // skip editor for commit msg
|
|
123
|
+
const { stderr } = await execFileAsync("git", [op, "--continue"], { cwd, env }).then((r) => ({ stdout: r.stdout, stderr: "" }), (e) => ({
|
|
124
|
+
stdout: "",
|
|
125
|
+
stderr: (e.stderr ?? e.message ?? "").trim(),
|
|
126
|
+
}));
|
|
127
|
+
if (stderr && !stderr.includes("Successfully")) {
|
|
128
|
+
continueSpinner.fail(`Could not continue ${op}: ${stderr}`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
continueSpinner.succeed(`${op.charAt(0).toUpperCase() + op.slice(1)} completed ✓`);
|
|
133
|
+
// Rebase rewrites history; merge does not
|
|
134
|
+
await pushAfterSync(cwd, op === "rebase");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// ── Normal sync flow ───────────────────────────────────────────────────
|
|
138
|
+
const head = await getCurrentBranch(cwd);
|
|
139
|
+
// Resolve base
|
|
140
|
+
let base;
|
|
141
|
+
if (opts.base) {
|
|
142
|
+
base = opts.base;
|
|
143
|
+
logger.info(`📌 Base branch (provided): ${base}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const detectSpinner = ora("Detecting base branch…").start();
|
|
147
|
+
base = await detectBaseBranch(cwd);
|
|
148
|
+
detectSpinner.succeed(`Base branch: ${base}`);
|
|
149
|
+
}
|
|
150
|
+
if (head === base) {
|
|
151
|
+
logger.info(`✨ Already on the base branch "${base}" — nothing to sync.`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// ── Check for unresolved PR review comments BEFORE syncing ────────────
|
|
155
|
+
// If the current branch has an open PR with unresolved inline comments,
|
|
156
|
+
// offer to resolve them now. Fixes are committed onto the branch; the
|
|
157
|
+
// sync then rebases/merges and pushes everything together.
|
|
158
|
+
await checkAndOfferAddressComments(cwd, head);
|
|
159
|
+
logger.info(`\n🔄 Syncing ${head} onto origin/${base}\n`);
|
|
160
|
+
// Fetch latest
|
|
161
|
+
const fetchSpinner = ora("Fetching latest from origin…").start();
|
|
162
|
+
const { stderr: fetchErr } = await git(["fetch", "origin"], cwd);
|
|
163
|
+
if (fetchErr && !fetchErr.includes("->")) {
|
|
164
|
+
fetchSpinner.fail(`Fetch failed: ${fetchErr}`);
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
fetchSpinner.succeed("Fetched latest.");
|
|
169
|
+
// Check if we're already up to date
|
|
170
|
+
const { stdout: behindCount } = await git(["rev-list", "--count", `HEAD..origin/${base}`], cwd);
|
|
171
|
+
if (behindCount === "0") {
|
|
172
|
+
logger.success(`✅ "${head}" is already up to date with origin/${base}. Ready to merge!`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Run rebase or merge
|
|
176
|
+
const syncSpinner = ora(strategy === "rebase"
|
|
177
|
+
? `Rebasing ${head} onto origin/${base}…`
|
|
178
|
+
: `Merging origin/${base} into ${head}…`).start();
|
|
179
|
+
const syncArgs = strategy === "rebase"
|
|
180
|
+
? ["rebase", `origin/${base}`]
|
|
181
|
+
: ["merge", `origin/${base}`, "--no-edit"];
|
|
182
|
+
const { stderr: syncErr } = await git(syncArgs, cwd);
|
|
183
|
+
// Detect conflict — try AI resolution first
|
|
184
|
+
const conflicts = await getConflictingFiles(cwd);
|
|
185
|
+
if (conflicts.length > 0) {
|
|
186
|
+
syncSpinner.fail(`Conflicts detected in ${conflicts.length} file(s) — attempting AI resolution…`);
|
|
187
|
+
// Try to load an AI client
|
|
188
|
+
let gitx = null;
|
|
189
|
+
try {
|
|
190
|
+
gitx = await Gitx.fromCwd(cwd);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// No AI available; fall through to manual instructions
|
|
194
|
+
}
|
|
195
|
+
if (gitx && await Gitx.isAiAvailable(gitx.config)) {
|
|
196
|
+
const resolved = [];
|
|
197
|
+
const needsManual = [];
|
|
198
|
+
for (const filePath of conflicts) {
|
|
199
|
+
const absPath = resolvePath(cwd, filePath);
|
|
200
|
+
let content;
|
|
201
|
+
try {
|
|
202
|
+
content = await readFile(absPath, "utf8");
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
needsManual.push(filePath);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Skip binary files (no conflict markers)
|
|
209
|
+
if (!content.includes("<<<<<<<")) {
|
|
210
|
+
needsManual.push(filePath);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const resolveSpinner = ora(` 🤖 AI resolving: ${filePath}`).start();
|
|
214
|
+
try {
|
|
215
|
+
const result = await gitx.ai.resolveConflict(filePath, content);
|
|
216
|
+
if (result.confidence === "high") {
|
|
217
|
+
await writeFile(absPath, result.resolved, "utf8");
|
|
218
|
+
resolveSpinner.succeed(` ✅ Auto-resolved: ${filePath} — ${result.explanation}`);
|
|
219
|
+
resolved.push(filePath);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
resolveSpinner.warn(` ⚠️ Low confidence: ${filePath} — ${result.explanation}`);
|
|
223
|
+
logger.info(`\n AI proposed resolution (low confidence). Preview:\n`);
|
|
224
|
+
// Show first 40 lines of the resolved content as a preview
|
|
225
|
+
const preview = result.resolved.split("\n").slice(0, 40).join("\n");
|
|
226
|
+
logger.info(preview);
|
|
227
|
+
if (result.resolved.split("\n").length > 40) {
|
|
228
|
+
logger.info(` … (${result.resolved.split("\n").length - 40} more lines)`);
|
|
229
|
+
}
|
|
230
|
+
logger.info("");
|
|
231
|
+
let apply = false;
|
|
232
|
+
try {
|
|
233
|
+
apply = await confirm({
|
|
234
|
+
message: `Apply AI resolution for ${filePath}?`,
|
|
235
|
+
default: true,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
apply = false;
|
|
240
|
+
}
|
|
241
|
+
if (apply) {
|
|
242
|
+
await writeFile(absPath, result.resolved, "utf8");
|
|
243
|
+
logger.success(` ✅ Applied: ${filePath}`);
|
|
244
|
+
resolved.push(filePath);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
logger.info(` ⏭️ Skipped: ${filePath} — resolve manually`);
|
|
248
|
+
needsManual.push(filePath);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
resolveSpinner.fail(` ❌ AI resolution failed for: ${filePath}`);
|
|
254
|
+
needsManual.push(filePath);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (needsManual.length > 0) {
|
|
258
|
+
logger.error(`\n⚠️ ${needsManual.length} file(s) still need manual resolution:\n`);
|
|
259
|
+
for (const f of needsManual)
|
|
260
|
+
logger.info(` • ${f}`);
|
|
261
|
+
logger.info(`\n Steps to finish:\n`);
|
|
262
|
+
logger.info(` 1. Open each file and fix the conflict markers (<<<<, ====, >>>>)`);
|
|
263
|
+
logger.info(` 2. Mark resolved: git add <file>`);
|
|
264
|
+
logger.info(` 3. Finish sync: gitx sync --continue`);
|
|
265
|
+
logger.info(` 4. Retry merge: gitx pr merge <number>\n`);
|
|
266
|
+
logger.info(` To give up and go back: gitx sync --abort\n`);
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// All resolved — stage and continue
|
|
271
|
+
if (resolved.length > 0) {
|
|
272
|
+
logger.success(`\n✅ AI resolved all ${resolved.length} conflict(s). Staging and continuing…\n`);
|
|
273
|
+
await git(["add", "-A"], cwd);
|
|
274
|
+
const env = { ...process.env, GIT_EDITOR: "true" };
|
|
275
|
+
const { stderr: contErr } = await execFileAsync("git", [strategy === "rebase" ? "rebase" : "merge", "--continue"], { cwd, env }).then((r) => ({ stdout: r.stdout, stderr: "" }), (e) => ({
|
|
276
|
+
stdout: "",
|
|
277
|
+
stderr: (e.stderr ?? e.message ?? "").trim(),
|
|
278
|
+
}));
|
|
279
|
+
if (contErr && !contErr.toLowerCase().includes("successfully")) {
|
|
280
|
+
logger.error(`Could not continue ${strategy}: ${contErr}`);
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// No AI — fall back to manual instructions
|
|
288
|
+
logger.error(`\n⚠️ Merge conflicts in ${conflicts.length} file(s):\n`);
|
|
289
|
+
for (const f of conflicts)
|
|
290
|
+
logger.info(` • ${f}`);
|
|
291
|
+
logger.info(`\n Steps to resolve:\n`);
|
|
292
|
+
logger.info(` 1. Open each file and fix the conflict markers (<<<<, ====, >>>>)`);
|
|
293
|
+
logger.info(` 2. Mark resolved: git add <file>`);
|
|
294
|
+
logger.info(` 3. Finish sync: gitx sync --continue`);
|
|
295
|
+
logger.info(` 4. Retry merge: gitx pr merge <number>\n`);
|
|
296
|
+
logger.info(` To give up and go back: gitx sync --abort\n`);
|
|
297
|
+
process.exitCode = 1;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (syncErr && !syncErr.toLowerCase().includes("successfully")) {
|
|
302
|
+
syncSpinner.fail(`Sync failed: ${syncErr}`);
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
syncSpinner.succeed(strategy === "rebase"
|
|
307
|
+
? `Rebased ${head} onto origin/${base} ✓`
|
|
308
|
+
: `Merged origin/${base} into ${head} ✓`);
|
|
309
|
+
// Rebase rewrites history → force-with-lease; merge → plain push
|
|
310
|
+
await pushAfterSync(cwd, strategy === "rebase");
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Before syncing, look up any open PR for the current branch.
|
|
315
|
+
* If it has unresolved inline review comments, ask the user:
|
|
316
|
+
* - "Resolve comments first, then sync" → address + commit, sync continues
|
|
317
|
+
* - "Sync normally" → proceed immediately
|
|
318
|
+
*
|
|
319
|
+
* Fixes are committed using "commit-no-push" mode so the sync rebase/merge
|
|
320
|
+
* picks them up and pushes everything together in a single push.
|
|
321
|
+
*/
|
|
322
|
+
async function checkAndOfferAddressComments(cwd, currentBranch) {
|
|
323
|
+
let gitx = null;
|
|
324
|
+
try {
|
|
325
|
+
gitx = await Gitx.fromCwd(cwd);
|
|
326
|
+
if (!await Gitx.isAiAvailable(gitx.config))
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return; // no gitx config — skip silently
|
|
331
|
+
}
|
|
332
|
+
let prNumber = null;
|
|
333
|
+
let unresolvedCount = 0;
|
|
334
|
+
try {
|
|
335
|
+
const ctx = await gitx.getRepoContext();
|
|
336
|
+
const provider = createProvider(ctx);
|
|
337
|
+
// Find the open PR for the current branch
|
|
338
|
+
const prs = await provider.listPRs(ctx.repoSlug);
|
|
339
|
+
const openPr = prs.find((p) => p.head === currentBranch && p.state === "open");
|
|
340
|
+
if (!openPr)
|
|
341
|
+
return;
|
|
342
|
+
prNumber = openPr.number;
|
|
343
|
+
// Use the shared helper: root inline comments with no "✅ Addressed" reply yet
|
|
344
|
+
const allComments = await provider.getPRComments(ctx.repoSlug, prNumber);
|
|
345
|
+
unresolvedCount = filterUnresolvedInlineComments(allComments).length;
|
|
346
|
+
if (unresolvedCount === 0)
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return; // provider error — don't block the sync
|
|
351
|
+
}
|
|
352
|
+
// ── Surface the choice ──────────────────────────────────────────────────────
|
|
353
|
+
logger.info(`\n💬 PR #${prNumber} has ${unresolvedCount} unresolved review comment(s).\n`);
|
|
354
|
+
let choice;
|
|
355
|
+
try {
|
|
356
|
+
choice = await select({
|
|
357
|
+
message: "How would you like to proceed?",
|
|
358
|
+
choices: [
|
|
359
|
+
{
|
|
360
|
+
name: `Resolve comments first, then sync (AI generates fixes → you approve → commit → sync)`,
|
|
361
|
+
value: "resolve",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: `Sync normally (skip comment resolution, proceed with merge)`,
|
|
365
|
+
value: "skip",
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return; // Ctrl-C → skip
|
|
372
|
+
}
|
|
373
|
+
if (choice === "skip") {
|
|
374
|
+
logger.info("⏭️ Skipping comment resolution — proceeding with sync.\n");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// ── Resolve comments (commit but don't push — sync handles the push) ───────
|
|
378
|
+
logger.info(`\n🔧 Resolving ${unresolvedCount} review comment(s) on PR #${prNumber}…\n`);
|
|
379
|
+
try {
|
|
380
|
+
const result = await runAddressWorkflow(gitx, prNumber, { mode: "commit-no-push" });
|
|
381
|
+
const applied = result.addressed.filter((a) => a.applied).length;
|
|
382
|
+
const skipped = result.addressed.filter((a) => a.skipped).length;
|
|
383
|
+
if (applied > 0) {
|
|
384
|
+
logger.success(`✅ ${applied} fix(es) committed.${skipped > 0 ? ` (${skipped} skipped)` : ""}`);
|
|
385
|
+
logger.info(" Sync will rebase these commits and push everything together.\n");
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
logger.info(" No fixes applied — continuing with normal sync.\n");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
logger.warn(`⚠️ Comment resolution error: ${err.message}\n Continuing with sync.\n`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function pushAfterSync(cwd, forceWithLease = false) {
|
|
396
|
+
// Rebase rewrites history → requires --force-with-lease.
|
|
397
|
+
// Merge does not rewrite history → plain push is fine.
|
|
398
|
+
const pushArgs = forceWithLease
|
|
399
|
+
? ["push", "--force-with-lease"]
|
|
400
|
+
: ["push"];
|
|
401
|
+
const label = forceWithLease ? "Pushing (force-with-lease)…" : "Pushing…";
|
|
402
|
+
const pushSpinner = ora(label).start();
|
|
403
|
+
const { stderr } = await git(pushArgs, cwd);
|
|
404
|
+
if (stderr && stderr.includes("error")) {
|
|
405
|
+
pushSpinner.fail(`Push failed: ${stderr}`);
|
|
406
|
+
const hint = forceWithLease ? "git push --force-with-lease" : "git push";
|
|
407
|
+
logger.info(` Try: ${hint}`);
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
pushSpinner.succeed("Pushed ✓");
|
|
412
|
+
logger.success(`\n✅ Branch is now up to date. Run \`gitx pr merge <number>\` to merge.\n`);
|
|
413
|
+
}
|
|
414
|
+
//# sourceMappingURL=sync.js.map
|