episoda 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth.d.ts +22 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +384 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/dev.d.ts +20 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +305 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/status.d.ts +9 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +75 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +17 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +81 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/core/auth.d.ts +26 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +113 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/command-protocol.d.ts +262 -0
- package/dist/core/command-protocol.d.ts.map +1 -0
- package/dist/core/command-protocol.js +13 -0
- package/dist/core/command-protocol.js.map +1 -0
- package/dist/core/connection-manager.d.ts +58 -0
- package/dist/core/connection-manager.d.ts.map +1 -0
- package/dist/core/connection-manager.js +215 -0
- package/dist/core/connection-manager.js.map +1 -0
- package/dist/core/errors.d.ts +18 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +55 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git-executor.d.ts +157 -0
- package/dist/core/git-executor.d.ts.map +1 -0
- package/dist/core/git-executor.js +1605 -0
- package/dist/core/git-executor.js.map +1 -0
- package/dist/core/git-parser.d.ts +40 -0
- package/dist/core/git-parser.d.ts.map +1 -0
- package/dist/core/git-parser.js +194 -0
- package/dist/core/git-parser.js.map +1 -0
- package/dist/core/git-validator.d.ts +42 -0
- package/dist/core/git-validator.d.ts.map +1 -0
- package/dist/core/git-validator.js +102 -0
- package/dist/core/git-validator.js.map +1 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +41 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/version.d.ts +9 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +19 -0
- package/dist/core/version.js.map +1 -0
- package/dist/core/websocket-client.d.ts +122 -0
- package/dist/core/websocket-client.d.ts.map +1 -0
- package/dist/core/websocket-client.js +438 -0
- package/dist/core/websocket-client.js.map +1 -0
- package/dist/daemon/daemon-manager.d.ts +71 -0
- package/dist/daemon/daemon-manager.d.ts.map +1 -0
- package/dist/daemon/daemon-manager.js +289 -0
- package/dist/daemon/daemon-manager.js.map +1 -0
- package/dist/daemon/daemon-process.d.ts +13 -0
- package/dist/daemon/daemon-process.d.ts.map +1 -0
- package/dist/daemon/daemon-process.js +608 -0
- package/dist/daemon/daemon-process.js.map +1 -0
- package/dist/daemon/machine-id.d.ts +36 -0
- package/dist/daemon/machine-id.d.ts.map +1 -0
- package/dist/daemon/machine-id.js +195 -0
- package/dist/daemon/machine-id.js.map +1 -0
- package/dist/daemon/project-tracker.d.ts +92 -0
- package/dist/daemon/project-tracker.d.ts.map +1 -0
- package/dist/daemon/project-tracker.js +259 -0
- package/dist/daemon/project-tracker.js.map +1 -0
- package/dist/dev-wrapper.d.ts +88 -0
- package/dist/dev-wrapper.d.ts.map +1 -0
- package/dist/dev-wrapper.js +288 -0
- package/dist/dev-wrapper.js.map +1 -0
- package/dist/framework-detector.d.ts +29 -0
- package/dist/framework-detector.d.ts.map +1 -0
- package/dist/framework-detector.js +276 -0
- package/dist/framework-detector.js.map +1 -0
- package/dist/git-helpers/git-credential-helper.d.ts +29 -0
- package/dist/git-helpers/git-credential-helper.d.ts.map +1 -0
- package/dist/git-helpers/git-credential-helper.js +349 -0
- package/dist/git-helpers/git-credential-helper.js.map +1 -0
- package/dist/hooks/post-checkout +296 -0
- package/dist/hooks/pre-commit +139 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/ipc-client.d.ts +95 -0
- package/dist/ipc/ipc-client.d.ts.map +1 -0
- package/dist/ipc/ipc-client.js +204 -0
- package/dist/ipc/ipc-client.js.map +1 -0
- package/dist/ipc/ipc-server.d.ts +55 -0
- package/dist/ipc/ipc-server.d.ts.map +1 -0
- package/dist/ipc/ipc-server.js +177 -0
- package/dist/ipc/ipc-server.js.map +1 -0
- package/dist/output.d.ts +48 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +129 -0
- package/dist/output.js.map +1 -0
- package/dist/utils/port-check.d.ts +15 -0
- package/dist/utils/port-check.d.ts.map +1 -0
- package/dist/utils/port-check.js +79 -0
- package/dist/utils/port-check.js.map +1 -0
- package/dist/utils/update-checker.d.ts +23 -0
- package/dist/utils/update-checker.d.ts.map +1 -0
- package/dist/utils/update-checker.js +95 -0
- package/dist/utils/update-checker.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Git Command Executor
|
|
4
|
+
*
|
|
5
|
+
* Executes git commands with error handling and returns structured results.
|
|
6
|
+
* Interface-agnostic: Returns structured data, not formatted strings.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.GitExecutor = void 0;
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const util_1 = require("util");
|
|
45
|
+
const git_validator_1 = require("./git-validator");
|
|
46
|
+
const git_parser_1 = require("./git-parser");
|
|
47
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
48
|
+
/**
|
|
49
|
+
* Executes git commands with error handling
|
|
50
|
+
*
|
|
51
|
+
* DESIGN PRINCIPLES:
|
|
52
|
+
* - Interface-agnostic: Returns structured data, not formatted strings
|
|
53
|
+
* - No console.log: Let the interface handle output
|
|
54
|
+
* - Error codes: Return error codes, not messages
|
|
55
|
+
* - Reusable: Can be used by CLI, MCP, or any other interface
|
|
56
|
+
*/
|
|
57
|
+
class GitExecutor {
|
|
58
|
+
/**
|
|
59
|
+
* Execute a git command
|
|
60
|
+
* @param command - The git command to execute
|
|
61
|
+
* @param options - Execution options (timeout, cwd, etc.)
|
|
62
|
+
* @returns Structured result with success/error details
|
|
63
|
+
*/
|
|
64
|
+
async execute(command, options) {
|
|
65
|
+
try {
|
|
66
|
+
// Validate git is installed
|
|
67
|
+
const gitInstalled = await this.validateGitInstalled();
|
|
68
|
+
if (!gitInstalled) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: 'GIT_NOT_INSTALLED'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Determine working directory
|
|
75
|
+
const cwd = options?.cwd || process.cwd();
|
|
76
|
+
// Validate this is a git repository (except for init-like commands)
|
|
77
|
+
const isGitRepo = await this.isGitRepository(cwd);
|
|
78
|
+
if (!isGitRepo) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'NOT_GIT_REPO'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Route to appropriate handler based on action
|
|
85
|
+
switch (command.action) {
|
|
86
|
+
case 'checkout':
|
|
87
|
+
return await this.executeCheckout(command, cwd, options);
|
|
88
|
+
case 'create_branch':
|
|
89
|
+
return await this.executeCreateBranch(command, cwd, options);
|
|
90
|
+
case 'commit':
|
|
91
|
+
return await this.executeCommit(command, cwd, options);
|
|
92
|
+
case 'push':
|
|
93
|
+
return await this.executePush(command, cwd, options);
|
|
94
|
+
case 'status':
|
|
95
|
+
return await this.executeStatus(cwd, options);
|
|
96
|
+
case 'pull':
|
|
97
|
+
return await this.executePull(command, cwd, options);
|
|
98
|
+
case 'delete_branch':
|
|
99
|
+
return await this.executeDeleteBranch(command, cwd, options);
|
|
100
|
+
// EP597: Read operations for production local dev mode
|
|
101
|
+
case 'branch_exists':
|
|
102
|
+
return await this.executeBranchExists(command, cwd, options);
|
|
103
|
+
case 'branch_has_commits':
|
|
104
|
+
return await this.executeBranchHasCommits(command, cwd, options);
|
|
105
|
+
// EP598: Main branch check for production
|
|
106
|
+
case 'main_branch_check':
|
|
107
|
+
return await this.executeMainBranchCheck(cwd, options);
|
|
108
|
+
// EP599: Get commits for branch
|
|
109
|
+
case 'get_commits':
|
|
110
|
+
return await this.executeGetCommits(command, cwd, options);
|
|
111
|
+
// EP599: Advanced operations for move-to-module and discard-main-changes
|
|
112
|
+
case 'stash':
|
|
113
|
+
return await this.executeStash(command, cwd, options);
|
|
114
|
+
case 'reset':
|
|
115
|
+
return await this.executeReset(command, cwd, options);
|
|
116
|
+
case 'merge':
|
|
117
|
+
return await this.executeMerge(command, cwd, options);
|
|
118
|
+
case 'cherry_pick':
|
|
119
|
+
return await this.executeCherryPick(command, cwd, options);
|
|
120
|
+
case 'clean':
|
|
121
|
+
return await this.executeClean(command, cwd, options);
|
|
122
|
+
case 'add':
|
|
123
|
+
return await this.executeAdd(command, cwd, options);
|
|
124
|
+
case 'fetch':
|
|
125
|
+
return await this.executeFetch(command, cwd, options);
|
|
126
|
+
// EP599-3: Composite operations
|
|
127
|
+
case 'move_to_module':
|
|
128
|
+
return await this.executeMoveToModule(command, cwd, options);
|
|
129
|
+
case 'discard_main_changes':
|
|
130
|
+
return await this.executeDiscardMainChanges(cwd, options);
|
|
131
|
+
// EP523: Branch sync operations
|
|
132
|
+
case 'sync_status':
|
|
133
|
+
return await this.executeSyncStatus(command, cwd, options);
|
|
134
|
+
case 'sync_main':
|
|
135
|
+
return await this.executeSyncMain(cwd, options);
|
|
136
|
+
case 'rebase_branch':
|
|
137
|
+
return await this.executeRebaseBranch(command, cwd, options);
|
|
138
|
+
case 'rebase_abort':
|
|
139
|
+
return await this.executeRebaseAbort(cwd, options);
|
|
140
|
+
case 'rebase_continue':
|
|
141
|
+
return await this.executeRebaseContinue(cwd, options);
|
|
142
|
+
case 'rebase_status':
|
|
143
|
+
return await this.executeRebaseStatus(cwd, options);
|
|
144
|
+
default:
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
error: 'UNKNOWN_ERROR',
|
|
148
|
+
output: 'Unknown command action'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: 'UNKNOWN_ERROR',
|
|
156
|
+
output: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Execute checkout command
|
|
162
|
+
*/
|
|
163
|
+
async executeCheckout(command, cwd, options) {
|
|
164
|
+
// Validate branch name
|
|
165
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
166
|
+
if (!validation.valid) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Check for uncommitted changes first
|
|
173
|
+
const statusResult = await this.executeStatus(cwd, options);
|
|
174
|
+
if (statusResult.success && statusResult.details?.uncommittedFiles?.length) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: 'UNCOMMITTED_CHANGES',
|
|
178
|
+
details: {
|
|
179
|
+
uncommittedFiles: statusResult.details.uncommittedFiles
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// Build command
|
|
184
|
+
const args = ['checkout'];
|
|
185
|
+
if (command.create) {
|
|
186
|
+
args.push('-b');
|
|
187
|
+
}
|
|
188
|
+
args.push(command.branch);
|
|
189
|
+
return await this.runGitCommand(args, cwd, options);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Execute create_branch command
|
|
193
|
+
*/
|
|
194
|
+
async executeCreateBranch(command, cwd, options) {
|
|
195
|
+
// Validate branch name
|
|
196
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
197
|
+
if (!validation.valid) {
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Validate source branch if provided
|
|
204
|
+
if (command.from) {
|
|
205
|
+
const fromValidation = (0, git_validator_1.validateBranchName)(command.from);
|
|
206
|
+
if (!fromValidation.valid) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: fromValidation.error || 'UNKNOWN_ERROR'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Build command - use checkout -b to create AND checkout the branch
|
|
214
|
+
// This ensures the user is on the new branch immediately
|
|
215
|
+
const args = ['checkout', '-b', command.branch];
|
|
216
|
+
if (command.from) {
|
|
217
|
+
args.push(command.from);
|
|
218
|
+
}
|
|
219
|
+
return await this.runGitCommand(args, cwd, options);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Execute commit command
|
|
223
|
+
*/
|
|
224
|
+
async executeCommit(command, cwd, options) {
|
|
225
|
+
// Validate commit message
|
|
226
|
+
const validation = (0, git_validator_1.validateCommitMessage)(command.message);
|
|
227
|
+
if (!validation.valid) {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// Validate file paths if provided
|
|
234
|
+
if (command.files) {
|
|
235
|
+
const fileValidation = (0, git_validator_1.validateFilePaths)(command.files);
|
|
236
|
+
if (!fileValidation.valid) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
error: fileValidation.error || 'UNKNOWN_ERROR'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Stage specific files first
|
|
243
|
+
for (const file of command.files) {
|
|
244
|
+
const addResult = await this.runGitCommand(['add', file], cwd, options);
|
|
245
|
+
if (!addResult.success) {
|
|
246
|
+
return addResult;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// Stage all changes
|
|
252
|
+
const addResult = await this.runGitCommand(['add', '-A'], cwd, options);
|
|
253
|
+
if (!addResult.success) {
|
|
254
|
+
return addResult;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Execute commit
|
|
258
|
+
const args = ['commit', '-m', command.message];
|
|
259
|
+
return await this.runGitCommand(args, cwd, options);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Execute push command
|
|
263
|
+
* EP769: Added force parameter for pushing rebased branches
|
|
264
|
+
*/
|
|
265
|
+
async executePush(command, cwd, options) {
|
|
266
|
+
// Validate branch name
|
|
267
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
268
|
+
if (!validation.valid) {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// Build command
|
|
275
|
+
const args = ['push'];
|
|
276
|
+
// EP769: Add --force flag for rebased branches
|
|
277
|
+
if (command.force) {
|
|
278
|
+
args.push('--force');
|
|
279
|
+
}
|
|
280
|
+
if (command.setUpstream) {
|
|
281
|
+
args.push('-u', 'origin', command.branch);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
args.push('origin', command.branch);
|
|
285
|
+
}
|
|
286
|
+
// Configure git credential helper for GitHub token if provided
|
|
287
|
+
const env = { ...process.env };
|
|
288
|
+
if (options?.githubToken) {
|
|
289
|
+
env.GIT_ASKPASS = 'echo';
|
|
290
|
+
env.GIT_USERNAME = 'x-access-token';
|
|
291
|
+
env.GIT_PASSWORD = options.githubToken;
|
|
292
|
+
}
|
|
293
|
+
return await this.runGitCommand(args, cwd, { ...options, env });
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Execute status command
|
|
297
|
+
*/
|
|
298
|
+
async executeStatus(cwd, options) {
|
|
299
|
+
const result = await this.runGitCommand(['status', '--porcelain', '-b'], cwd, options);
|
|
300
|
+
if (result.success && result.output) {
|
|
301
|
+
const statusInfo = (0, git_parser_1.parseGitStatus)(result.output);
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
output: result.output,
|
|
305
|
+
details: {
|
|
306
|
+
uncommittedFiles: statusInfo.uncommittedFiles,
|
|
307
|
+
branchName: statusInfo.currentBranch
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Execute pull command
|
|
315
|
+
*/
|
|
316
|
+
async executePull(command, cwd, options) {
|
|
317
|
+
// Validate branch name if provided
|
|
318
|
+
if (command.branch) {
|
|
319
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
320
|
+
if (!validation.valid) {
|
|
321
|
+
return {
|
|
322
|
+
success: false,
|
|
323
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Check for uncommitted changes first
|
|
328
|
+
const statusResult = await this.executeStatus(cwd, options);
|
|
329
|
+
if (statusResult.success && statusResult.details?.uncommittedFiles?.length) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: 'UNCOMMITTED_CHANGES',
|
|
333
|
+
details: {
|
|
334
|
+
uncommittedFiles: statusResult.details.uncommittedFiles
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// Build command
|
|
339
|
+
const args = ['pull'];
|
|
340
|
+
if (command.branch) {
|
|
341
|
+
args.push('origin', command.branch);
|
|
342
|
+
}
|
|
343
|
+
const result = await this.runGitCommand(args, cwd, options);
|
|
344
|
+
// Check for merge conflicts
|
|
345
|
+
if (!result.success && result.output) {
|
|
346
|
+
const conflicts = (0, git_parser_1.parseMergeConflicts)(result.output);
|
|
347
|
+
if (conflicts.length > 0) {
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
error: 'MERGE_CONFLICT',
|
|
351
|
+
output: result.output,
|
|
352
|
+
details: {
|
|
353
|
+
conflictingFiles: conflicts
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Execute delete_branch command
|
|
362
|
+
*/
|
|
363
|
+
async executeDeleteBranch(command, cwd, options) {
|
|
364
|
+
// Validate branch name
|
|
365
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
366
|
+
if (!validation.valid) {
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// Build command
|
|
373
|
+
const args = ['branch'];
|
|
374
|
+
args.push(command.force ? '-D' : '-d');
|
|
375
|
+
args.push(command.branch);
|
|
376
|
+
return await this.runGitCommand(args, cwd, options);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* EP597: Execute branch_exists command
|
|
380
|
+
* Checks if a branch exists locally and/or remotely
|
|
381
|
+
*/
|
|
382
|
+
async executeBranchExists(command, cwd, options) {
|
|
383
|
+
// Validate branch name
|
|
384
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
385
|
+
if (!validation.valid) {
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
let isLocal = false;
|
|
393
|
+
let isRemote = false;
|
|
394
|
+
// Check local branches
|
|
395
|
+
try {
|
|
396
|
+
const { stdout: localBranches } = await execAsync('git branch --list', { cwd, timeout: options?.timeout || 10000 });
|
|
397
|
+
isLocal = localBranches.split('\n').some(line => {
|
|
398
|
+
const branchName = line.replace(/^\*?\s*/, '').trim();
|
|
399
|
+
return branchName === command.branch;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// Ignore errors - branch doesn't exist locally
|
|
404
|
+
}
|
|
405
|
+
// Check remote branches
|
|
406
|
+
try {
|
|
407
|
+
const { stdout: remoteBranches } = await execAsync(`git ls-remote --heads origin ${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
|
|
408
|
+
isRemote = remoteBranches.trim().length > 0;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Ignore errors - can't check remote (might be network issue)
|
|
412
|
+
}
|
|
413
|
+
const branchExists = isLocal || isRemote;
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
output: branchExists ? `Branch ${command.branch} exists` : `Branch ${command.branch} does not exist`,
|
|
417
|
+
details: {
|
|
418
|
+
branchName: command.branch,
|
|
419
|
+
branchExists,
|
|
420
|
+
isLocal,
|
|
421
|
+
isRemote
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
error: 'UNKNOWN_ERROR',
|
|
429
|
+
output: error.message || 'Failed to check branch existence'
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* EP597: Execute branch_has_commits command
|
|
435
|
+
* Checks if a branch has commits ahead of the base branch (default: main)
|
|
436
|
+
*/
|
|
437
|
+
async executeBranchHasCommits(command, cwd, options) {
|
|
438
|
+
// Validate branch name
|
|
439
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
440
|
+
if (!validation.valid) {
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const baseBranch = command.baseBranch || 'main';
|
|
447
|
+
try {
|
|
448
|
+
// Use git cherry to find commits unique to the branch
|
|
449
|
+
// This shows commits on branch that aren't on base
|
|
450
|
+
const { stdout } = await execAsync(`git cherry origin/${baseBranch} ${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
|
|
451
|
+
// git cherry shows lines starting with + for unique commits
|
|
452
|
+
const uniqueCommits = stdout.trim().split('\n').filter(line => line.startsWith('+'));
|
|
453
|
+
const hasCommits = uniqueCommits.length > 0;
|
|
454
|
+
return {
|
|
455
|
+
success: true,
|
|
456
|
+
output: hasCommits
|
|
457
|
+
? `Branch ${command.branch} has ${uniqueCommits.length} commits ahead of ${baseBranch}`
|
|
458
|
+
: `Branch ${command.branch} has no commits ahead of ${baseBranch}`,
|
|
459
|
+
details: {
|
|
460
|
+
branchName: command.branch,
|
|
461
|
+
hasCommits
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
// If git cherry fails (branch not found, etc.), try alternative method
|
|
467
|
+
try {
|
|
468
|
+
// Alternative: count commits with rev-list
|
|
469
|
+
const { stdout } = await execAsync(`git rev-list --count origin/${baseBranch}..${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
|
|
470
|
+
const commitCount = parseInt(stdout.trim(), 10);
|
|
471
|
+
const hasCommits = commitCount > 0;
|
|
472
|
+
return {
|
|
473
|
+
success: true,
|
|
474
|
+
output: hasCommits
|
|
475
|
+
? `Branch ${command.branch} has ${commitCount} commits ahead of ${baseBranch}`
|
|
476
|
+
: `Branch ${command.branch} has no commits ahead of ${baseBranch}`,
|
|
477
|
+
details: {
|
|
478
|
+
branchName: command.branch,
|
|
479
|
+
hasCommits
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// Both methods failed - branch likely doesn't exist or isn't tracked
|
|
485
|
+
return {
|
|
486
|
+
success: false,
|
|
487
|
+
error: 'BRANCH_NOT_FOUND',
|
|
488
|
+
output: error.message || `Failed to check commits for branch ${command.branch}`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* EP598: Execute main branch check - returns current branch, uncommitted files, and unpushed commits
|
|
495
|
+
*/
|
|
496
|
+
async executeMainBranchCheck(cwd, options) {
|
|
497
|
+
try {
|
|
498
|
+
// Get current branch
|
|
499
|
+
let currentBranch = '';
|
|
500
|
+
try {
|
|
501
|
+
const { stdout } = await execAsync('git branch --show-current', { cwd, timeout: options?.timeout || 10000 });
|
|
502
|
+
currentBranch = stdout.trim();
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
error: 'UNKNOWN_ERROR',
|
|
508
|
+
output: error.message || 'Failed to get current branch'
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// Get uncommitted files
|
|
512
|
+
let uncommittedFiles = [];
|
|
513
|
+
try {
|
|
514
|
+
const { stdout } = await execAsync('git status --porcelain', { cwd, timeout: options?.timeout || 10000 });
|
|
515
|
+
if (stdout) {
|
|
516
|
+
uncommittedFiles = stdout.split('\n').filter(line => line.trim()).map(line => {
|
|
517
|
+
const parts = line.trim().split(/\s+/);
|
|
518
|
+
return parts.slice(1).join(' ');
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Ignore errors - just means no uncommitted changes
|
|
524
|
+
}
|
|
525
|
+
// Get unpushed commits (only if on main branch)
|
|
526
|
+
let localCommits = [];
|
|
527
|
+
if (currentBranch === 'main') {
|
|
528
|
+
try {
|
|
529
|
+
const { stdout } = await execAsync('git log origin/main..HEAD --format="%H|%s|%an"', { cwd, timeout: options?.timeout || 10000 });
|
|
530
|
+
if (stdout) {
|
|
531
|
+
localCommits = stdout.split('\n').filter(line => line.trim()).map(line => {
|
|
532
|
+
const [sha, message, author] = line.split('|');
|
|
533
|
+
return {
|
|
534
|
+
sha: sha ? sha.substring(0, 8) : '',
|
|
535
|
+
message: message || '',
|
|
536
|
+
author: author || ''
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// Ignore errors - might not have origin/main or no remote
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
success: true,
|
|
547
|
+
output: `Branch: ${currentBranch}, Uncommitted: ${uncommittedFiles.length}, Unpushed: ${localCommits.length}`,
|
|
548
|
+
details: {
|
|
549
|
+
currentBranch,
|
|
550
|
+
uncommittedFiles,
|
|
551
|
+
localCommits
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
return {
|
|
557
|
+
success: false,
|
|
558
|
+
error: 'UNKNOWN_ERROR',
|
|
559
|
+
output: error.message || 'Failed to check main branch'
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* EP599: Execute get_commits command
|
|
565
|
+
* Returns commits for a branch with pushed/unpushed status
|
|
566
|
+
*/
|
|
567
|
+
async executeGetCommits(command, cwd, options) {
|
|
568
|
+
// Validate branch name
|
|
569
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
570
|
+
if (!validation.valid) {
|
|
571
|
+
return {
|
|
572
|
+
success: false,
|
|
573
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const limit = command.limit || 10;
|
|
577
|
+
const baseBranch = command.baseBranch || 'main';
|
|
578
|
+
try {
|
|
579
|
+
// Get commits unique to this branch (not in main)
|
|
580
|
+
let stdout;
|
|
581
|
+
try {
|
|
582
|
+
const result = await execAsync(`git log ${baseBranch}.."${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 10000 });
|
|
583
|
+
stdout = result.stdout;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
// Fallback: if comparison fails, show all commits on this branch
|
|
587
|
+
try {
|
|
588
|
+
const result = await execAsync(`git log "${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 10000 });
|
|
589
|
+
stdout = result.stdout;
|
|
590
|
+
}
|
|
591
|
+
catch (branchError) {
|
|
592
|
+
// Branch doesn't exist locally
|
|
593
|
+
return {
|
|
594
|
+
success: false,
|
|
595
|
+
error: 'BRANCH_NOT_FOUND',
|
|
596
|
+
output: `Branch ${command.branch} not found locally`
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (!stdout.trim()) {
|
|
601
|
+
return {
|
|
602
|
+
success: true,
|
|
603
|
+
output: 'No commits found',
|
|
604
|
+
details: {
|
|
605
|
+
commits: []
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
// Parse commits
|
|
610
|
+
const commitLines = stdout.trim().split('\n');
|
|
611
|
+
// Check which commits have been pushed to remote
|
|
612
|
+
let remoteShas = new Set();
|
|
613
|
+
try {
|
|
614
|
+
const { stdout: remoteCommits } = await execAsync(`git log "origin/${command.branch}" --pretty=format:"%H" -n ${limit} --`, { cwd, timeout: options?.timeout || 10000 });
|
|
615
|
+
remoteShas = new Set(remoteCommits.trim().split('\n').filter(Boolean));
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
// Remote branch doesn't exist - all commits are local/unpushed
|
|
619
|
+
}
|
|
620
|
+
const commits = commitLines.map((line) => {
|
|
621
|
+
const [sha, authorName, authorEmail, date, ...messageParts] = line.split('|');
|
|
622
|
+
const message = messageParts.join('|'); // Handle pipes in commit messages
|
|
623
|
+
const isPushed = remoteShas.has(sha);
|
|
624
|
+
return {
|
|
625
|
+
sha,
|
|
626
|
+
message,
|
|
627
|
+
authorName,
|
|
628
|
+
authorEmail,
|
|
629
|
+
date,
|
|
630
|
+
isPushed
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
success: true,
|
|
635
|
+
output: `Found ${commits.length} commits`,
|
|
636
|
+
details: {
|
|
637
|
+
commits
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
error: 'UNKNOWN_ERROR',
|
|
645
|
+
output: error.message || 'Failed to get commits'
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// ========================================
|
|
650
|
+
// EP599: Advanced operations for move-to-module and discard-main-changes
|
|
651
|
+
// ========================================
|
|
652
|
+
/**
|
|
653
|
+
* Execute git stash operations
|
|
654
|
+
*/
|
|
655
|
+
async executeStash(command, cwd, options) {
|
|
656
|
+
try {
|
|
657
|
+
const args = ['stash'];
|
|
658
|
+
switch (command.operation) {
|
|
659
|
+
case 'push':
|
|
660
|
+
args.push('push');
|
|
661
|
+
if (command.includeUntracked) {
|
|
662
|
+
args.push('--include-untracked');
|
|
663
|
+
}
|
|
664
|
+
if (command.message) {
|
|
665
|
+
args.push('-m', command.message);
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
case 'pop':
|
|
669
|
+
args.push('pop');
|
|
670
|
+
break;
|
|
671
|
+
case 'drop':
|
|
672
|
+
args.push('drop');
|
|
673
|
+
break;
|
|
674
|
+
case 'list':
|
|
675
|
+
args.push('list');
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
return await this.runGitCommand(args, cwd, options);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
return {
|
|
682
|
+
success: false,
|
|
683
|
+
error: 'UNKNOWN_ERROR',
|
|
684
|
+
output: error.message || 'Stash operation failed'
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Execute git reset
|
|
690
|
+
*/
|
|
691
|
+
async executeReset(command, cwd, options) {
|
|
692
|
+
try {
|
|
693
|
+
const args = ['reset', `--${command.mode}`];
|
|
694
|
+
if (command.target) {
|
|
695
|
+
args.push(command.target);
|
|
696
|
+
}
|
|
697
|
+
return await this.runGitCommand(args, cwd, options);
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
return {
|
|
701
|
+
success: false,
|
|
702
|
+
error: 'UNKNOWN_ERROR',
|
|
703
|
+
output: error.message || 'Reset failed'
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Execute git merge
|
|
709
|
+
*/
|
|
710
|
+
async executeMerge(command, cwd, options) {
|
|
711
|
+
try {
|
|
712
|
+
if (command.abort) {
|
|
713
|
+
return await this.runGitCommand(['merge', '--abort'], cwd, options);
|
|
714
|
+
}
|
|
715
|
+
// Validate branch name
|
|
716
|
+
const branchValidation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
717
|
+
if (!branchValidation.valid) {
|
|
718
|
+
return {
|
|
719
|
+
success: false,
|
|
720
|
+
error: 'BRANCH_NOT_FOUND',
|
|
721
|
+
output: branchValidation.error || 'Invalid branch name'
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
const args = ['merge', command.branch];
|
|
725
|
+
if (command.strategy === 'ours') {
|
|
726
|
+
args.push('--strategy=ours');
|
|
727
|
+
}
|
|
728
|
+
else if (command.strategy === 'theirs') {
|
|
729
|
+
args.push('--strategy-option=theirs');
|
|
730
|
+
}
|
|
731
|
+
if (command.noEdit) {
|
|
732
|
+
args.push('--no-edit');
|
|
733
|
+
}
|
|
734
|
+
const result = await this.runGitCommand(args, cwd, options);
|
|
735
|
+
// Check for merge conflicts
|
|
736
|
+
if (!result.success && result.output?.includes('CONFLICT')) {
|
|
737
|
+
result.details = result.details || {};
|
|
738
|
+
result.details.mergeConflicts = true;
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
return {
|
|
744
|
+
success: false,
|
|
745
|
+
error: 'MERGE_CONFLICT',
|
|
746
|
+
output: error.message || 'Merge failed'
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Execute git cherry-pick
|
|
752
|
+
*/
|
|
753
|
+
async executeCherryPick(command, cwd, options) {
|
|
754
|
+
try {
|
|
755
|
+
if (command.abort) {
|
|
756
|
+
return await this.runGitCommand(['cherry-pick', '--abort'], cwd, options);
|
|
757
|
+
}
|
|
758
|
+
const result = await this.runGitCommand(['cherry-pick', command.sha], cwd, options);
|
|
759
|
+
// Check for cherry-pick conflicts
|
|
760
|
+
if (!result.success && result.output?.includes('CONFLICT')) {
|
|
761
|
+
result.details = result.details || {};
|
|
762
|
+
result.details.cherryPickConflicts = true;
|
|
763
|
+
}
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
return {
|
|
768
|
+
success: false,
|
|
769
|
+
error: 'MERGE_CONFLICT',
|
|
770
|
+
output: error.message || 'Cherry-pick failed'
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Execute git clean
|
|
776
|
+
*/
|
|
777
|
+
async executeClean(command, cwd, options) {
|
|
778
|
+
try {
|
|
779
|
+
const args = ['clean'];
|
|
780
|
+
if (command.force) {
|
|
781
|
+
args.push('-f');
|
|
782
|
+
}
|
|
783
|
+
if (command.directories) {
|
|
784
|
+
args.push('-d');
|
|
785
|
+
}
|
|
786
|
+
return await this.runGitCommand(args, cwd, options);
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
return {
|
|
790
|
+
success: false,
|
|
791
|
+
error: 'UNKNOWN_ERROR',
|
|
792
|
+
output: error.message || 'Clean failed'
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Execute git add
|
|
798
|
+
*/
|
|
799
|
+
async executeAdd(command, cwd, options) {
|
|
800
|
+
try {
|
|
801
|
+
const args = ['add'];
|
|
802
|
+
if (command.all) {
|
|
803
|
+
args.push('-A');
|
|
804
|
+
}
|
|
805
|
+
else if (command.files && command.files.length > 0) {
|
|
806
|
+
// Validate file paths
|
|
807
|
+
const validation = (0, git_validator_1.validateFilePaths)(command.files);
|
|
808
|
+
if (!validation.valid) {
|
|
809
|
+
return {
|
|
810
|
+
success: false,
|
|
811
|
+
error: 'UNKNOWN_ERROR',
|
|
812
|
+
output: validation.error || 'Invalid file paths'
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
args.push(...command.files);
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
args.push('-A'); // Default to all
|
|
819
|
+
}
|
|
820
|
+
return await this.runGitCommand(args, cwd, options);
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
return {
|
|
824
|
+
success: false,
|
|
825
|
+
error: 'UNKNOWN_ERROR',
|
|
826
|
+
output: error.message || 'Add failed'
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Execute git fetch
|
|
832
|
+
*/
|
|
833
|
+
async executeFetch(command, cwd, options) {
|
|
834
|
+
try {
|
|
835
|
+
const args = ['fetch'];
|
|
836
|
+
if (command.remote) {
|
|
837
|
+
args.push(command.remote);
|
|
838
|
+
if (command.branch) {
|
|
839
|
+
args.push(command.branch);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
args.push('origin');
|
|
844
|
+
}
|
|
845
|
+
return await this.runGitCommand(args, cwd, options);
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
return {
|
|
849
|
+
success: false,
|
|
850
|
+
error: 'NETWORK_ERROR',
|
|
851
|
+
output: error.message || 'Fetch failed'
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// ========================================
|
|
856
|
+
// EP599-3: Composite operations
|
|
857
|
+
// ========================================
|
|
858
|
+
/**
|
|
859
|
+
* Execute move_to_module - composite operation
|
|
860
|
+
* Moves commits/changes to a module branch with conflict handling
|
|
861
|
+
*/
|
|
862
|
+
async executeMoveToModule(command, cwd, options) {
|
|
863
|
+
const { targetBranch, commitShas, conflictResolution } = command;
|
|
864
|
+
let hasStash = false;
|
|
865
|
+
const cherryPickedCommits = [];
|
|
866
|
+
try {
|
|
867
|
+
// Step 1: Get current branch
|
|
868
|
+
const { stdout: currentBranchOut } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
|
|
869
|
+
const currentBranch = currentBranchOut.trim();
|
|
870
|
+
// Step 2: Stash uncommitted changes
|
|
871
|
+
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd });
|
|
872
|
+
if (statusOutput.trim()) {
|
|
873
|
+
try {
|
|
874
|
+
await execAsync('git add -A', { cwd });
|
|
875
|
+
const { stdout: stashHash } = await execAsync('git stash create -m "episoda-move-to-module"', { cwd });
|
|
876
|
+
if (stashHash && stashHash.trim()) {
|
|
877
|
+
await execAsync(`git stash store -m "episoda-move-to-module" ${stashHash.trim()}`, { cwd });
|
|
878
|
+
await execAsync('git reset --hard HEAD', { cwd });
|
|
879
|
+
hasStash = true;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (stashError) {
|
|
883
|
+
// Continue without stashing
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// Step 3: Switch to main if we have commits to move
|
|
887
|
+
if (commitShas && commitShas.length > 0 && currentBranch !== 'main' && currentBranch !== 'master') {
|
|
888
|
+
await execAsync('git checkout main', { cwd });
|
|
889
|
+
}
|
|
890
|
+
// Step 4: Create or checkout target branch
|
|
891
|
+
let branchExists = false;
|
|
892
|
+
try {
|
|
893
|
+
await execAsync(`git rev-parse --verify ${targetBranch}`, { cwd });
|
|
894
|
+
branchExists = true;
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
branchExists = false;
|
|
898
|
+
}
|
|
899
|
+
if (!branchExists) {
|
|
900
|
+
await execAsync(`git checkout -b ${targetBranch}`, { cwd });
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
await execAsync(`git checkout ${targetBranch}`, { cwd });
|
|
904
|
+
// Try to merge changes from main
|
|
905
|
+
if (currentBranch === 'main' || currentBranch === 'master') {
|
|
906
|
+
try {
|
|
907
|
+
const mergeStrategy = conflictResolution === 'ours' ? '--strategy=ours' :
|
|
908
|
+
conflictResolution === 'theirs' ? '--strategy-option=theirs' : '';
|
|
909
|
+
await execAsync(`git merge ${currentBranch} ${mergeStrategy} --no-edit`, { cwd });
|
|
910
|
+
}
|
|
911
|
+
catch (mergeError) {
|
|
912
|
+
// Check for conflicts
|
|
913
|
+
const { stdout: conflictStatus } = await execAsync('git status --porcelain', { cwd });
|
|
914
|
+
if (conflictStatus.includes('UU ') || conflictStatus.includes('AA ') || conflictStatus.includes('DD ')) {
|
|
915
|
+
const { stdout: conflictFiles } = await execAsync('git diff --name-only --diff-filter=U', { cwd });
|
|
916
|
+
const conflictedFiles = conflictFiles.trim().split('\n').filter(Boolean);
|
|
917
|
+
if (conflictResolution) {
|
|
918
|
+
// Auto-resolve conflicts
|
|
919
|
+
for (const file of conflictedFiles) {
|
|
920
|
+
await execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
|
|
921
|
+
await execAsync(`git add "${file}"`, { cwd });
|
|
922
|
+
}
|
|
923
|
+
await execAsync('git commit --no-edit', { cwd });
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
// Abort merge and return conflict error
|
|
927
|
+
await execAsync('git merge --abort', { cwd });
|
|
928
|
+
return {
|
|
929
|
+
success: false,
|
|
930
|
+
error: 'MERGE_CONFLICT',
|
|
931
|
+
output: 'Merge conflicts detected',
|
|
932
|
+
details: {
|
|
933
|
+
hasConflicts: true,
|
|
934
|
+
conflictedFiles,
|
|
935
|
+
movedToBranch: targetBranch
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// Step 5: Cherry-pick commits if provided
|
|
944
|
+
if (commitShas && commitShas.length > 0 && (currentBranch === 'main' || currentBranch === 'master')) {
|
|
945
|
+
for (const sha of commitShas) {
|
|
946
|
+
try {
|
|
947
|
+
// Check if commit already exists in branch
|
|
948
|
+
const { stdout: logOutput } = await execAsync(`git log --format=%H ${targetBranch} | grep ${sha}`, { cwd }).catch(() => ({ stdout: '' }));
|
|
949
|
+
if (!logOutput.trim()) {
|
|
950
|
+
await execAsync(`git cherry-pick ${sha}`, { cwd });
|
|
951
|
+
cherryPickedCommits.push(sha);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
await execAsync('git cherry-pick --abort', { cwd }).catch(() => { });
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
// Reset main to origin/main
|
|
959
|
+
await execAsync('git checkout main', { cwd });
|
|
960
|
+
await execAsync('git reset --hard origin/main', { cwd });
|
|
961
|
+
await execAsync(`git checkout ${targetBranch}`, { cwd });
|
|
962
|
+
}
|
|
963
|
+
// Step 6: Apply stashed changes
|
|
964
|
+
if (hasStash) {
|
|
965
|
+
try {
|
|
966
|
+
await execAsync('git stash pop', { cwd });
|
|
967
|
+
}
|
|
968
|
+
catch (stashError) {
|
|
969
|
+
// Check for stash conflicts
|
|
970
|
+
const { stdout: conflictStatus } = await execAsync('git status --porcelain', { cwd });
|
|
971
|
+
if (conflictStatus.includes('UU ') || conflictStatus.includes('AA ')) {
|
|
972
|
+
if (conflictResolution) {
|
|
973
|
+
const { stdout: conflictFiles } = await execAsync('git diff --name-only --diff-filter=U', { cwd });
|
|
974
|
+
const conflictedFiles = conflictFiles.trim().split('\n').filter(Boolean);
|
|
975
|
+
for (const file of conflictedFiles) {
|
|
976
|
+
await execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
|
|
977
|
+
await execAsync(`git add "${file}"`, { cwd });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
success: true,
|
|
985
|
+
output: `Successfully moved to branch ${targetBranch}`,
|
|
986
|
+
details: {
|
|
987
|
+
movedToBranch: targetBranch,
|
|
988
|
+
cherryPickedCommits,
|
|
989
|
+
currentBranch: targetBranch
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
catch (error) {
|
|
994
|
+
// Try to restore stash if something went wrong
|
|
995
|
+
if (hasStash) {
|
|
996
|
+
try {
|
|
997
|
+
const { stdout: stashList } = await execAsync('git stash list', { cwd });
|
|
998
|
+
if (stashList.includes('episoda-move-to-module')) {
|
|
999
|
+
await execAsync('git stash pop', { cwd });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
catch (e) {
|
|
1003
|
+
// Ignore errors restoring stash
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
success: false,
|
|
1008
|
+
error: 'UNKNOWN_ERROR',
|
|
1009
|
+
output: error.message || 'Move to module failed'
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Execute discard_main_changes - composite operation
|
|
1015
|
+
* Discards all uncommitted files and local commits on main branch
|
|
1016
|
+
*/
|
|
1017
|
+
async executeDiscardMainChanges(cwd, options) {
|
|
1018
|
+
try {
|
|
1019
|
+
// Step 1: Verify we're on main/master
|
|
1020
|
+
const { stdout: currentBranchOut } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
|
|
1021
|
+
const branch = currentBranchOut.trim();
|
|
1022
|
+
if (branch !== 'main' && branch !== 'master') {
|
|
1023
|
+
return {
|
|
1024
|
+
success: false,
|
|
1025
|
+
error: 'BRANCH_NOT_FOUND',
|
|
1026
|
+
output: `Cannot discard changes - not on main branch. Current branch: ${branch}`
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
let discardedFiles = 0;
|
|
1030
|
+
// Step 2: Stash uncommitted changes (will be dropped)
|
|
1031
|
+
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd });
|
|
1032
|
+
if (statusOutput.trim()) {
|
|
1033
|
+
try {
|
|
1034
|
+
await execAsync('git stash --include-untracked', { cwd });
|
|
1035
|
+
discardedFiles = statusOutput.trim().split('\n').length;
|
|
1036
|
+
}
|
|
1037
|
+
catch (stashError) {
|
|
1038
|
+
// Continue - might be nothing to stash
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
// Step 3: Fetch and reset to origin
|
|
1042
|
+
await execAsync('git fetch origin', { cwd });
|
|
1043
|
+
await execAsync(`git reset --hard origin/${branch}`, { cwd });
|
|
1044
|
+
// Step 4: Clean untracked files
|
|
1045
|
+
try {
|
|
1046
|
+
await execAsync('git clean -fd', { cwd });
|
|
1047
|
+
}
|
|
1048
|
+
catch (cleanError) {
|
|
1049
|
+
// Non-critical
|
|
1050
|
+
}
|
|
1051
|
+
// Step 5: Drop the stash
|
|
1052
|
+
try {
|
|
1053
|
+
await execAsync('git stash drop', { cwd });
|
|
1054
|
+
}
|
|
1055
|
+
catch (dropError) {
|
|
1056
|
+
// Stash might not exist
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
success: true,
|
|
1060
|
+
output: `Successfully discarded all changes and reset to origin/${branch}`,
|
|
1061
|
+
details: {
|
|
1062
|
+
currentBranch: branch,
|
|
1063
|
+
discardedFiles
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
return {
|
|
1069
|
+
success: false,
|
|
1070
|
+
error: 'UNKNOWN_ERROR',
|
|
1071
|
+
output: error.message || 'Discard main changes failed'
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// ========================================
|
|
1076
|
+
// EP523: Branch sync operations
|
|
1077
|
+
// ========================================
|
|
1078
|
+
/**
|
|
1079
|
+
* EP523: Get sync status of a branch relative to main
|
|
1080
|
+
* Returns how many commits behind/ahead the branch is
|
|
1081
|
+
*/
|
|
1082
|
+
async executeSyncStatus(command, cwd, options) {
|
|
1083
|
+
try {
|
|
1084
|
+
// Validate branch name
|
|
1085
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
1086
|
+
if (!validation.valid) {
|
|
1087
|
+
return {
|
|
1088
|
+
success: false,
|
|
1089
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
// Fetch latest from remote to get accurate counts
|
|
1093
|
+
try {
|
|
1094
|
+
await execAsync('git fetch origin', { cwd, timeout: options?.timeout || 30000 });
|
|
1095
|
+
}
|
|
1096
|
+
catch (fetchError) {
|
|
1097
|
+
// Network error - return what we can determine locally
|
|
1098
|
+
return {
|
|
1099
|
+
success: false,
|
|
1100
|
+
error: 'NETWORK_ERROR',
|
|
1101
|
+
output: 'Unable to fetch from remote. Check your network connection.'
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
let commitsBehind = 0;
|
|
1105
|
+
let commitsAhead = 0;
|
|
1106
|
+
// Count commits the branch is BEHIND main (main has commits branch doesn't)
|
|
1107
|
+
try {
|
|
1108
|
+
const { stdout: behindOutput } = await execAsync(`git rev-list --count ${command.branch}..origin/main`, { cwd, timeout: options?.timeout || 10000 });
|
|
1109
|
+
commitsBehind = parseInt(behindOutput.trim(), 10) || 0;
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
// Branch might not exist or no common ancestor
|
|
1113
|
+
commitsBehind = 0;
|
|
1114
|
+
}
|
|
1115
|
+
// Count commits the branch is AHEAD of main (branch has commits main doesn't)
|
|
1116
|
+
try {
|
|
1117
|
+
const { stdout: aheadOutput } = await execAsync(`git rev-list --count origin/main..${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
|
|
1118
|
+
commitsAhead = parseInt(aheadOutput.trim(), 10) || 0;
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
// Branch might not exist or no common ancestor
|
|
1122
|
+
commitsAhead = 0;
|
|
1123
|
+
}
|
|
1124
|
+
const isBehind = commitsBehind > 0;
|
|
1125
|
+
const isAhead = commitsAhead > 0;
|
|
1126
|
+
const needsSync = isBehind;
|
|
1127
|
+
return {
|
|
1128
|
+
success: true,
|
|
1129
|
+
output: isBehind
|
|
1130
|
+
? `Branch ${command.branch} is ${commitsBehind} commit(s) behind main`
|
|
1131
|
+
: `Branch ${command.branch} is up to date with main`,
|
|
1132
|
+
details: {
|
|
1133
|
+
branchName: command.branch,
|
|
1134
|
+
commitsBehind,
|
|
1135
|
+
commitsAhead,
|
|
1136
|
+
isBehind,
|
|
1137
|
+
isAhead,
|
|
1138
|
+
needsSync
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
catch (error) {
|
|
1143
|
+
return {
|
|
1144
|
+
success: false,
|
|
1145
|
+
error: 'UNKNOWN_ERROR',
|
|
1146
|
+
output: error.message || 'Failed to check sync status'
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* EP523: Sync local main branch with remote
|
|
1152
|
+
* Used before creating new branches to ensure we branch from latest main
|
|
1153
|
+
*/
|
|
1154
|
+
async executeSyncMain(cwd, options) {
|
|
1155
|
+
try {
|
|
1156
|
+
// Get current branch to restore later if needed
|
|
1157
|
+
let currentBranch = '';
|
|
1158
|
+
try {
|
|
1159
|
+
const { stdout } = await execAsync('git branch --show-current', { cwd, timeout: 5000 });
|
|
1160
|
+
currentBranch = stdout.trim();
|
|
1161
|
+
}
|
|
1162
|
+
catch {
|
|
1163
|
+
// Ignore - might be detached HEAD
|
|
1164
|
+
}
|
|
1165
|
+
// Fetch latest from remote
|
|
1166
|
+
try {
|
|
1167
|
+
await execAsync('git fetch origin main', { cwd, timeout: options?.timeout || 30000 });
|
|
1168
|
+
}
|
|
1169
|
+
catch (fetchError) {
|
|
1170
|
+
return {
|
|
1171
|
+
success: false,
|
|
1172
|
+
error: 'NETWORK_ERROR',
|
|
1173
|
+
output: 'Unable to fetch from remote. Check your network connection.'
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
// Check if we need to switch to main
|
|
1177
|
+
const needsSwitch = currentBranch !== 'main' && currentBranch !== '';
|
|
1178
|
+
if (needsSwitch) {
|
|
1179
|
+
// Check for uncommitted changes first
|
|
1180
|
+
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd, timeout: 5000 });
|
|
1181
|
+
if (statusOutput.trim()) {
|
|
1182
|
+
return {
|
|
1183
|
+
success: false,
|
|
1184
|
+
error: 'UNCOMMITTED_CHANGES',
|
|
1185
|
+
output: 'Cannot sync main: you have uncommitted changes. Commit or stash them first.',
|
|
1186
|
+
details: {
|
|
1187
|
+
uncommittedFiles: statusOutput.trim().split('\n').map(line => line.slice(3))
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
// Switch to main
|
|
1192
|
+
await execAsync('git checkout main', { cwd, timeout: options?.timeout || 10000 });
|
|
1193
|
+
}
|
|
1194
|
+
// Pull latest main
|
|
1195
|
+
try {
|
|
1196
|
+
await execAsync('git pull origin main', { cwd, timeout: options?.timeout || 30000 });
|
|
1197
|
+
}
|
|
1198
|
+
catch (pullError) {
|
|
1199
|
+
// Check for conflicts
|
|
1200
|
+
if (pullError.message?.includes('CONFLICT') || pullError.stderr?.includes('CONFLICT')) {
|
|
1201
|
+
// Abort the merge
|
|
1202
|
+
await execAsync('git merge --abort', { cwd, timeout: 5000 }).catch(() => { });
|
|
1203
|
+
return {
|
|
1204
|
+
success: false,
|
|
1205
|
+
error: 'MERGE_CONFLICT',
|
|
1206
|
+
output: 'Conflict while syncing main. This is unexpected - main should not have local commits.'
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
throw pullError;
|
|
1210
|
+
}
|
|
1211
|
+
// Switch back to original branch if we switched
|
|
1212
|
+
if (needsSwitch && currentBranch) {
|
|
1213
|
+
await execAsync(`git checkout "${currentBranch}"`, { cwd, timeout: options?.timeout || 10000 });
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
success: true,
|
|
1217
|
+
output: 'Successfully synced main with remote',
|
|
1218
|
+
details: {
|
|
1219
|
+
currentBranch: needsSwitch ? currentBranch : 'main'
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
catch (error) {
|
|
1224
|
+
return {
|
|
1225
|
+
success: false,
|
|
1226
|
+
error: 'UNKNOWN_ERROR',
|
|
1227
|
+
output: error.message || 'Failed to sync main'
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* EP523: Rebase a branch onto main
|
|
1233
|
+
* Used when resuming work on a branch that's behind main
|
|
1234
|
+
*/
|
|
1235
|
+
async executeRebaseBranch(command, cwd, options) {
|
|
1236
|
+
try {
|
|
1237
|
+
// Validate branch name
|
|
1238
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
1239
|
+
if (!validation.valid) {
|
|
1240
|
+
return {
|
|
1241
|
+
success: false,
|
|
1242
|
+
error: validation.error || 'UNKNOWN_ERROR'
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
// Check for uncommitted changes
|
|
1246
|
+
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd, timeout: 5000 });
|
|
1247
|
+
if (statusOutput.trim()) {
|
|
1248
|
+
return {
|
|
1249
|
+
success: false,
|
|
1250
|
+
error: 'UNCOMMITTED_CHANGES',
|
|
1251
|
+
output: 'Cannot rebase: you have uncommitted changes. Commit or stash them first.',
|
|
1252
|
+
details: {
|
|
1253
|
+
uncommittedFiles: statusOutput.trim().split('\n').map(line => line.slice(3))
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
// Get current branch
|
|
1258
|
+
const { stdout: currentBranchOut } = await execAsync('git branch --show-current', { cwd, timeout: 5000 });
|
|
1259
|
+
const currentBranch = currentBranchOut.trim();
|
|
1260
|
+
// Ensure we're on the target branch
|
|
1261
|
+
if (currentBranch !== command.branch) {
|
|
1262
|
+
await execAsync(`git checkout "${command.branch}"`, { cwd, timeout: options?.timeout || 10000 });
|
|
1263
|
+
}
|
|
1264
|
+
// Fetch latest main
|
|
1265
|
+
await execAsync('git fetch origin main', { cwd, timeout: options?.timeout || 30000 });
|
|
1266
|
+
// Perform rebase
|
|
1267
|
+
try {
|
|
1268
|
+
await execAsync('git rebase origin/main', { cwd, timeout: options?.timeout || 60000 });
|
|
1269
|
+
}
|
|
1270
|
+
catch (rebaseError) {
|
|
1271
|
+
const errorOutput = (rebaseError.stderr || '') + (rebaseError.stdout || '');
|
|
1272
|
+
// Check for conflicts
|
|
1273
|
+
if (errorOutput.includes('CONFLICT') || errorOutput.includes('could not apply')) {
|
|
1274
|
+
// Get conflicting files
|
|
1275
|
+
let conflictFiles = [];
|
|
1276
|
+
try {
|
|
1277
|
+
const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd, timeout: 5000 });
|
|
1278
|
+
conflictFiles = conflictOutput.trim().split('\n').filter(Boolean);
|
|
1279
|
+
}
|
|
1280
|
+
catch {
|
|
1281
|
+
// Ignore - try alternate method
|
|
1282
|
+
try {
|
|
1283
|
+
const { stdout: statusOut } = await execAsync('git status --porcelain', { cwd, timeout: 5000 });
|
|
1284
|
+
conflictFiles = statusOut
|
|
1285
|
+
.trim()
|
|
1286
|
+
.split('\n')
|
|
1287
|
+
.filter(line => line.startsWith('UU ') || line.startsWith('AA ') || line.startsWith('DD '))
|
|
1288
|
+
.map(line => line.slice(3));
|
|
1289
|
+
}
|
|
1290
|
+
catch {
|
|
1291
|
+
// Couldn't get conflict files
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
success: false,
|
|
1296
|
+
error: 'REBASE_CONFLICT',
|
|
1297
|
+
output: `Rebase conflict in ${conflictFiles.length} file(s). Resolve conflicts then use rebase_continue, or use rebase_abort to cancel.`,
|
|
1298
|
+
details: {
|
|
1299
|
+
inRebase: true,
|
|
1300
|
+
rebaseConflicts: conflictFiles,
|
|
1301
|
+
hasConflicts: true,
|
|
1302
|
+
conflictedFiles: conflictFiles
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
throw rebaseError;
|
|
1307
|
+
}
|
|
1308
|
+
return {
|
|
1309
|
+
success: true,
|
|
1310
|
+
output: `Successfully rebased ${command.branch} onto main`,
|
|
1311
|
+
details: {
|
|
1312
|
+
branchName: command.branch,
|
|
1313
|
+
inRebase: false
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
return {
|
|
1319
|
+
success: false,
|
|
1320
|
+
error: 'UNKNOWN_ERROR',
|
|
1321
|
+
output: error.message || 'Rebase failed'
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* EP523: Abort an in-progress rebase
|
|
1327
|
+
* Returns to the state before rebase was started
|
|
1328
|
+
*/
|
|
1329
|
+
async executeRebaseAbort(cwd, options) {
|
|
1330
|
+
try {
|
|
1331
|
+
await execAsync('git rebase --abort', { cwd, timeout: options?.timeout || 10000 });
|
|
1332
|
+
return {
|
|
1333
|
+
success: true,
|
|
1334
|
+
output: 'Rebase aborted. Your branch has been restored to its previous state.',
|
|
1335
|
+
details: {
|
|
1336
|
+
inRebase: false
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
// Check if there's no rebase in progress
|
|
1342
|
+
if (error.message?.includes('No rebase in progress') || error.stderr?.includes('No rebase in progress')) {
|
|
1343
|
+
return {
|
|
1344
|
+
success: true,
|
|
1345
|
+
output: 'No rebase in progress.',
|
|
1346
|
+
details: {
|
|
1347
|
+
inRebase: false
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
success: false,
|
|
1353
|
+
error: 'UNKNOWN_ERROR',
|
|
1354
|
+
output: error.message || 'Failed to abort rebase'
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* EP523: Continue a paused rebase after conflicts are resolved
|
|
1360
|
+
*/
|
|
1361
|
+
async executeRebaseContinue(cwd, options) {
|
|
1362
|
+
try {
|
|
1363
|
+
// Stage all resolved files
|
|
1364
|
+
await execAsync('git add -A', { cwd, timeout: 5000 });
|
|
1365
|
+
// Continue the rebase
|
|
1366
|
+
await execAsync('git rebase --continue', { cwd, timeout: options?.timeout || 60000 });
|
|
1367
|
+
return {
|
|
1368
|
+
success: true,
|
|
1369
|
+
output: 'Rebase continued successfully.',
|
|
1370
|
+
details: {
|
|
1371
|
+
inRebase: false
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
catch (error) {
|
|
1376
|
+
const errorOutput = (error.stderr || '') + (error.stdout || '');
|
|
1377
|
+
// Check if there are still conflicts
|
|
1378
|
+
if (errorOutput.includes('CONFLICT') || errorOutput.includes('could not apply')) {
|
|
1379
|
+
// Get remaining conflict files
|
|
1380
|
+
let conflictFiles = [];
|
|
1381
|
+
try {
|
|
1382
|
+
const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd, timeout: 5000 });
|
|
1383
|
+
conflictFiles = conflictOutput.trim().split('\n').filter(Boolean);
|
|
1384
|
+
}
|
|
1385
|
+
catch {
|
|
1386
|
+
// Ignore
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
success: false,
|
|
1390
|
+
error: 'REBASE_CONFLICT',
|
|
1391
|
+
output: 'More conflicts encountered. Resolve them and try again.',
|
|
1392
|
+
details: {
|
|
1393
|
+
inRebase: true,
|
|
1394
|
+
rebaseConflicts: conflictFiles,
|
|
1395
|
+
hasConflicts: true,
|
|
1396
|
+
conflictedFiles: conflictFiles
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
// Check if there's no rebase in progress
|
|
1401
|
+
if (errorOutput.includes('No rebase in progress')) {
|
|
1402
|
+
return {
|
|
1403
|
+
success: true,
|
|
1404
|
+
output: 'No rebase in progress.',
|
|
1405
|
+
details: {
|
|
1406
|
+
inRebase: false
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
success: false,
|
|
1412
|
+
error: 'UNKNOWN_ERROR',
|
|
1413
|
+
output: error.message || 'Failed to continue rebase'
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* EP523: Check if a rebase is currently in progress
|
|
1419
|
+
*/
|
|
1420
|
+
async executeRebaseStatus(cwd, options) {
|
|
1421
|
+
try {
|
|
1422
|
+
// Check for rebase-merge directory (indicates rebase in progress)
|
|
1423
|
+
let inRebase = false;
|
|
1424
|
+
let rebaseConflicts = [];
|
|
1425
|
+
try {
|
|
1426
|
+
const { stdout: gitDir } = await execAsync('git rev-parse --git-dir', { cwd, timeout: 5000 });
|
|
1427
|
+
const gitDirPath = gitDir.trim();
|
|
1428
|
+
// Check for rebase directories
|
|
1429
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs'))).then(m => m.promises);
|
|
1430
|
+
const rebaseMergePath = `${gitDirPath}/rebase-merge`;
|
|
1431
|
+
const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
|
|
1432
|
+
try {
|
|
1433
|
+
await fs.access(rebaseMergePath);
|
|
1434
|
+
inRebase = true;
|
|
1435
|
+
}
|
|
1436
|
+
catch {
|
|
1437
|
+
try {
|
|
1438
|
+
await fs.access(rebaseApplyPath);
|
|
1439
|
+
inRebase = true;
|
|
1440
|
+
}
|
|
1441
|
+
catch {
|
|
1442
|
+
inRebase = false;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
catch {
|
|
1447
|
+
// If we can't determine, check via status
|
|
1448
|
+
try {
|
|
1449
|
+
const { stdout: statusOutput } = await execAsync('git status', { cwd, timeout: 5000 });
|
|
1450
|
+
inRebase = statusOutput.includes('rebase in progress') ||
|
|
1451
|
+
statusOutput.includes('interactive rebase in progress') ||
|
|
1452
|
+
statusOutput.includes('You are currently rebasing');
|
|
1453
|
+
}
|
|
1454
|
+
catch {
|
|
1455
|
+
inRebase = false;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
// If in rebase, get conflict files
|
|
1459
|
+
if (inRebase) {
|
|
1460
|
+
try {
|
|
1461
|
+
const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd, timeout: 5000 });
|
|
1462
|
+
rebaseConflicts = conflictOutput.trim().split('\n').filter(Boolean);
|
|
1463
|
+
}
|
|
1464
|
+
catch {
|
|
1465
|
+
// No conflicts or couldn't get them
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
success: true,
|
|
1470
|
+
output: inRebase
|
|
1471
|
+
? `Rebase in progress with ${rebaseConflicts.length} conflicting file(s)`
|
|
1472
|
+
: 'No rebase in progress',
|
|
1473
|
+
details: {
|
|
1474
|
+
inRebase,
|
|
1475
|
+
rebaseConflicts,
|
|
1476
|
+
hasConflicts: rebaseConflicts.length > 0
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
catch (error) {
|
|
1481
|
+
return {
|
|
1482
|
+
success: false,
|
|
1483
|
+
error: 'UNKNOWN_ERROR',
|
|
1484
|
+
output: error.message || 'Failed to check rebase status'
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Run a git command and return structured result
|
|
1490
|
+
*/
|
|
1491
|
+
async runGitCommand(args, cwd, options) {
|
|
1492
|
+
try {
|
|
1493
|
+
// Sanitize arguments
|
|
1494
|
+
const sanitizedArgs = (0, git_validator_1.sanitizeArgs)(args);
|
|
1495
|
+
// Build command
|
|
1496
|
+
const command = ['git', ...sanitizedArgs].join(' ');
|
|
1497
|
+
// Execute with timeout
|
|
1498
|
+
const timeout = options?.timeout || 30000; // 30 second default
|
|
1499
|
+
const execOptions = {
|
|
1500
|
+
cwd,
|
|
1501
|
+
timeout,
|
|
1502
|
+
env: options?.env || process.env,
|
|
1503
|
+
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
1504
|
+
};
|
|
1505
|
+
const { stdout, stderr } = await execAsync(command, execOptions);
|
|
1506
|
+
// Combine output
|
|
1507
|
+
const output = (stdout + stderr).trim();
|
|
1508
|
+
// Extract additional details
|
|
1509
|
+
const details = {};
|
|
1510
|
+
// Try to extract branch name
|
|
1511
|
+
const branchName = (0, git_parser_1.extractBranchName)(output);
|
|
1512
|
+
if (branchName) {
|
|
1513
|
+
details.branchName = branchName;
|
|
1514
|
+
}
|
|
1515
|
+
// Check for detached HEAD
|
|
1516
|
+
if ((0, git_parser_1.isDetachedHead)(output)) {
|
|
1517
|
+
details.branchName = 'HEAD (detached)';
|
|
1518
|
+
}
|
|
1519
|
+
return {
|
|
1520
|
+
success: true,
|
|
1521
|
+
output,
|
|
1522
|
+
details: Object.keys(details).length > 0 ? details : undefined
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
// Parse error
|
|
1527
|
+
const stderr = error.stderr || '';
|
|
1528
|
+
const stdout = error.stdout || '';
|
|
1529
|
+
const exitCode = error.code || 1;
|
|
1530
|
+
// Determine error code
|
|
1531
|
+
const errorCode = (0, git_parser_1.parseGitError)(stderr, stdout, exitCode);
|
|
1532
|
+
// Extract additional details based on error type
|
|
1533
|
+
const details = {
|
|
1534
|
+
exitCode
|
|
1535
|
+
};
|
|
1536
|
+
// Parse conflicts if merge conflict
|
|
1537
|
+
if (errorCode === 'MERGE_CONFLICT') {
|
|
1538
|
+
const conflicts = (0, git_parser_1.parseMergeConflicts)(stdout + stderr);
|
|
1539
|
+
if (conflicts.length > 0) {
|
|
1540
|
+
details.conflictingFiles = conflicts;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
// Parse status for uncommitted changes
|
|
1544
|
+
if (errorCode === 'UNCOMMITTED_CHANGES') {
|
|
1545
|
+
try {
|
|
1546
|
+
const statusResult = await this.executeStatus(cwd, options);
|
|
1547
|
+
if (statusResult.details?.uncommittedFiles) {
|
|
1548
|
+
details.uncommittedFiles = statusResult.details.uncommittedFiles;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
// Ignore errors when getting status
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return {
|
|
1556
|
+
success: false,
|
|
1557
|
+
error: errorCode,
|
|
1558
|
+
output: (stdout + stderr).trim(),
|
|
1559
|
+
details
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Validate that git is installed
|
|
1565
|
+
*/
|
|
1566
|
+
async validateGitInstalled() {
|
|
1567
|
+
try {
|
|
1568
|
+
await execAsync('git --version', { timeout: 5000 });
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
catch {
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Check if directory is a git repository
|
|
1577
|
+
*/
|
|
1578
|
+
async isGitRepository(cwd) {
|
|
1579
|
+
try {
|
|
1580
|
+
await execAsync('git rev-parse --git-dir', { cwd, timeout: 5000 });
|
|
1581
|
+
return true;
|
|
1582
|
+
}
|
|
1583
|
+
catch {
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Detect the git working directory (repository root)
|
|
1589
|
+
* @returns Path to the git repository root
|
|
1590
|
+
*/
|
|
1591
|
+
async detectWorkingDirectory(startPath) {
|
|
1592
|
+
try {
|
|
1593
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', {
|
|
1594
|
+
cwd: startPath || process.cwd(),
|
|
1595
|
+
timeout: 5000
|
|
1596
|
+
});
|
|
1597
|
+
return stdout.trim();
|
|
1598
|
+
}
|
|
1599
|
+
catch {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
exports.GitExecutor = GitExecutor;
|
|
1605
|
+
//# sourceMappingURL=git-executor.js.map
|