phantomx-tool-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tool-server.js +16 -0
- package/dist/Services/EditTool.d.ts +86 -0
- package/dist/Services/EditTool.d.ts.map +1 -0
- package/dist/Services/EditTool.js +516 -0
- package/dist/Services/EditTool.js.map +1 -0
- package/dist/Services/Logger.d.ts +32 -0
- package/dist/Services/Logger.d.ts.map +1 -0
- package/dist/Services/Logger.js +185 -0
- package/dist/Services/Logger.js.map +1 -0
- package/dist/Services/ReadFileTool.d.ts +49 -0
- package/dist/Services/ReadFileTool.d.ts.map +1 -0
- package/dist/Services/ReadFileTool.js +239 -0
- package/dist/Services/ReadFileTool.js.map +1 -0
- package/dist/__tests__/__mocks__/tool-server.mock.d.ts +4 -0
- package/dist/__tests__/__mocks__/tool-server.mock.d.ts.map +1 -0
- package/dist/__tests__/__mocks__/tool-server.mock.js +8 -0
- package/dist/__tests__/__mocks__/tool-server.mock.js.map +1 -0
- package/dist/__tests__/toolExecutionService_tempAI.test.d.ts +13 -0
- package/dist/__tests__/toolExecutionService_tempAI.test.d.ts.map +1 -0
- package/dist/__tests__/toolExecutionService_tempAI.test.js +491 -0
- package/dist/__tests__/toolExecutionService_tempAI.test.js.map +1 -0
- package/dist/githubOperationsHanlder.d.ts +274 -0
- package/dist/githubOperationsHanlder.d.ts.map +1 -0
- package/dist/githubOperationsHanlder.js +1487 -0
- package/dist/githubOperationsHanlder.js.map +1 -0
- package/dist/tool-server.d.ts +4 -0
- package/dist/tool-server.d.ts.map +1 -0
- package/dist/tool-server.js +190 -0
- package/dist/tool-server.js.map +1 -0
- package/dist/toolExecutionService.d.ts +28 -0
- package/dist/toolExecutionService.d.ts.map +1 -0
- package/dist/toolExecutionService.js +464 -0
- package/dist/toolExecutionService.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1487 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GithubOperationsService = void 0;
|
|
37
|
+
exports.getGithubOrganizationName = getGithubOrganizationName;
|
|
38
|
+
exports.fetchInstallationToken = fetchInstallationToken;
|
|
39
|
+
exports.getRepoList = getRepoList;
|
|
40
|
+
exports.getRepoBranch = getRepoBranch;
|
|
41
|
+
const Logger_1 = require("./Services/Logger");
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Local bash executor — drop-in replacement for ssh.executeCommand()
|
|
45
|
+
// Returns { success, output, error, code } matching the SSH client response.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
function executeShell(command) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const child = (0, child_process_1.spawn)('bash', ['-c', command], {
|
|
50
|
+
env: process.env,
|
|
51
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
52
|
+
});
|
|
53
|
+
let stdout = '';
|
|
54
|
+
let stderr = '';
|
|
55
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
56
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
57
|
+
child.on('close', (code) => {
|
|
58
|
+
resolve({ success: code === 0, output: stdout, error: stderr, code: code !== null && code !== void 0 ? code : 1 });
|
|
59
|
+
});
|
|
60
|
+
child.on('error', (err) => {
|
|
61
|
+
resolve({ success: false, output: '', error: err.message, code: 1 });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Use the official 'octokit' package import via require to avoid missing types for '@octokit/rest'
|
|
66
|
+
const { Octokit } = require('octokit');
|
|
67
|
+
const logger = (0, Logger_1.createLogger)('GithubOperationsService');
|
|
68
|
+
const simple_git_1 = require("simple-git");
|
|
69
|
+
const tool_server_1 = require("./tool-server");
|
|
70
|
+
async function getGithubOrganizationName() {
|
|
71
|
+
return tool_server_1.githubOrgName;
|
|
72
|
+
}
|
|
73
|
+
async function fetchInstallationToken() {
|
|
74
|
+
// this is not the installation token but a github PAT token for accessing the github repos
|
|
75
|
+
// fetching this token from an .env
|
|
76
|
+
return tool_server_1.githubPATToken;
|
|
77
|
+
}
|
|
78
|
+
async function getRepoList() {
|
|
79
|
+
let installationToken = await fetchInstallationToken(); //getting the installation Token
|
|
80
|
+
const resp = await fetch("https://api.github.com/installation/repositories", {
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: "application/vnd.github+json",
|
|
83
|
+
Authorization: `Bearer ${installationToken}`,
|
|
84
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
if (!resp.ok) {
|
|
88
|
+
throw new Error(`Failed to list repos: ${resp.status} ${await resp.text()}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await resp.json();
|
|
91
|
+
// data.repositories is an array of repo objects
|
|
92
|
+
return data.repositories.map((repo) => repo.name);
|
|
93
|
+
}
|
|
94
|
+
async function getRepoBranch(owner, repo) {
|
|
95
|
+
const installationToken = (await fetchInstallationToken()); //getting the installation Token
|
|
96
|
+
const octokit = new Octokit({ auth: installationToken });
|
|
97
|
+
try {
|
|
98
|
+
const branches = await octokit.paginate(octokit.rest.repos.listBranches, {
|
|
99
|
+
owner,
|
|
100
|
+
repo,
|
|
101
|
+
per_page: 100
|
|
102
|
+
});
|
|
103
|
+
return branches;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.log("Error listing branches:", err);
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
class GithubOperationsService {
|
|
111
|
+
escapeSingleQuotedShell(value) {
|
|
112
|
+
return value.replace(/'/g, `'\\''`);
|
|
113
|
+
}
|
|
114
|
+
async resolveBranchReference(repoPath, repoName, branchName) {
|
|
115
|
+
const requestedBranch = (branchName || '').trim();
|
|
116
|
+
if (!requestedBranch) {
|
|
117
|
+
throw new Error('Branch name is required');
|
|
118
|
+
}
|
|
119
|
+
const candidateRefs = requestedBranch.startsWith('origin/')
|
|
120
|
+
? [requestedBranch, requestedBranch.replace(/^origin\//, '')]
|
|
121
|
+
: [requestedBranch, `origin/${requestedBranch}`];
|
|
122
|
+
const uniqueCandidates = Array.from(new Set(candidateRefs.filter((candidate) => candidate.trim().length > 0)));
|
|
123
|
+
for (const candidate of uniqueCandidates) {
|
|
124
|
+
const safeCandidate = this.escapeSingleQuotedShell(candidate);
|
|
125
|
+
const branchCheck = await executeShell(`cd ${repoPath} && sudo git rev-parse --verify '${safeCandidate}^{commit}' >/dev/null 2>&1 && echo "BRANCH_OK"`);
|
|
126
|
+
if (branchCheck.success && (branchCheck.output || '').includes('BRANCH_OK')) {
|
|
127
|
+
return {
|
|
128
|
+
requestedBranch,
|
|
129
|
+
resolvedRef: candidate,
|
|
130
|
+
displayBranch: candidate.replace(/^origin\//, '')
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Branch '${requestedBranch}' not found in repository '${repoName}'`);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get repository list for an installation
|
|
138
|
+
*/
|
|
139
|
+
async getRepositoryList() {
|
|
140
|
+
const repositories = await getRepoList();
|
|
141
|
+
return {
|
|
142
|
+
repositories,
|
|
143
|
+
count: repositories.length
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* List branches for a repository
|
|
148
|
+
*/
|
|
149
|
+
async listBranches(owner, repo) {
|
|
150
|
+
logger.info(`Fetching branches for ${owner}/${repo} )`);
|
|
151
|
+
const branches = await getRepoBranch(owner, repo);
|
|
152
|
+
return {
|
|
153
|
+
repo,
|
|
154
|
+
branches,
|
|
155
|
+
count: branches.length
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Pull repository from remote
|
|
160
|
+
*/
|
|
161
|
+
async pullRepository(repoName, socket) {
|
|
162
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
163
|
+
const installationToken = await fetchInstallationToken();
|
|
164
|
+
const gitOrgName = await getGithubOrganizationName();
|
|
165
|
+
try {
|
|
166
|
+
// Get current local branch name
|
|
167
|
+
let result = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
|
|
168
|
+
if (!result.success) {
|
|
169
|
+
throw new Error(`Failed to get current branch: ${result.error}`);
|
|
170
|
+
}
|
|
171
|
+
const currentBranch = result.output.trim();
|
|
172
|
+
logger.info('starting git pull operation', { repoName, currentBranch });
|
|
173
|
+
// Set remote url with token
|
|
174
|
+
result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
|
|
175
|
+
if (!result.success) {
|
|
176
|
+
throw new Error(`Failed to set remote URL: ${result.error}`);
|
|
177
|
+
}
|
|
178
|
+
result = await executeShell(`cd ${repoPath} && sudo git fetch origin`);
|
|
179
|
+
if (!result.success) {
|
|
180
|
+
throw new Error(`Failed to fetch from remote: ${result.error}`);
|
|
181
|
+
}
|
|
182
|
+
result = await executeShell(`cd ${repoPath} && sudo git pull --no-rebase origin ${currentBranch}`);
|
|
183
|
+
if (!result.success && result.code !== 128) {
|
|
184
|
+
throw new Error(`Failed to pull branch ${currentBranch}: ${result.error}`);
|
|
185
|
+
}
|
|
186
|
+
await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
|
|
187
|
+
logger.success('Git pull completed successfully', { repoName, currentBranch });
|
|
188
|
+
// Get merge conflicts
|
|
189
|
+
const mergeConflicts = await this.getMergeConflicts(socket, repoName);
|
|
190
|
+
return {
|
|
191
|
+
repoName,
|
|
192
|
+
branchName: currentBranch,
|
|
193
|
+
lastCommit: result.output.trim(),
|
|
194
|
+
mergeConflicts
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Push repository to remote
|
|
202
|
+
*/
|
|
203
|
+
async pushRepository(repoName, socket) {
|
|
204
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
205
|
+
const installationToken = await fetchInstallationToken();
|
|
206
|
+
const gitOrgName = await getGithubOrganizationName();
|
|
207
|
+
try {
|
|
208
|
+
logger.info('Starting git push operation', { repoName });
|
|
209
|
+
// Combine all commands into a single SSH call
|
|
210
|
+
const combinedCommand = `
|
|
211
|
+
cd ${repoPath} &&
|
|
212
|
+
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
213
|
+
COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
214
|
+
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
215
|
+
echo "ERROR:NO_COMMITS";
|
|
216
|
+
exit 1;
|
|
217
|
+
fi &&
|
|
218
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
219
|
+
sudo git push origin $BRANCH &&
|
|
220
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
221
|
+
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
222
|
+
echo "BRANCH:$BRANCH" &&
|
|
223
|
+
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
224
|
+
echo "COMMIT_HASH:$COMMIT_HASH"
|
|
225
|
+
`.replace(/\n\s+/g, ' ');
|
|
226
|
+
const result = await executeShell(combinedCommand);
|
|
227
|
+
if (!result.success) {
|
|
228
|
+
if (result.output && result.output.includes('ERROR:NO_COMMITS')) {
|
|
229
|
+
throw new Error('No commits to push');
|
|
230
|
+
}
|
|
231
|
+
throw new Error('Failed to push repository');
|
|
232
|
+
}
|
|
233
|
+
// Parse output
|
|
234
|
+
const output = result.output;
|
|
235
|
+
const branchMatch = output.match(/BRANCH:([^\n]+)/);
|
|
236
|
+
const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
|
|
237
|
+
const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
|
|
238
|
+
const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
|
|
239
|
+
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
240
|
+
const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
|
|
241
|
+
logger.success('Git push completed successfully', { repoName, currentBranch, commitHash, commitsAhead });
|
|
242
|
+
// Emit socket event to refresh repo on client (only when called by AI agent)
|
|
243
|
+
if (socket) {
|
|
244
|
+
socket.emit('refresh_repo', { repoName });
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
repoName,
|
|
248
|
+
branchName: currentBranch,
|
|
249
|
+
commitHash,
|
|
250
|
+
commitsAhead,
|
|
251
|
+
pushedAt: new Date().toISOString()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Check repository status
|
|
259
|
+
*/
|
|
260
|
+
async checkRepositoryStatus(repoName, socket) {
|
|
261
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
262
|
+
const installationToken = await fetchInstallationToken();
|
|
263
|
+
const gitOrgName = await getGithubOrganizationName();
|
|
264
|
+
try {
|
|
265
|
+
logger.info('Checking repository status', { repoName });
|
|
266
|
+
// Combine all git commands into a single SSH call
|
|
267
|
+
const combinedCommand = `
|
|
268
|
+
cd ${repoPath} &&
|
|
269
|
+
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
270
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
271
|
+
sudo git fetch origin &&
|
|
272
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
273
|
+
BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
|
|
274
|
+
AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
275
|
+
STATUS=$(sudo git status --porcelain) &&
|
|
276
|
+
CURRENT_HASH=$(sudo git rev-parse HEAD) &&
|
|
277
|
+
REMOTE_HASH=$(sudo git rev-parse origin/$BRANCH 2>/dev/null || echo unknown) &&
|
|
278
|
+
echo "BRANCH:$BRANCH" &&
|
|
279
|
+
echo "BEHIND:$BEHIND" &&
|
|
280
|
+
echo "AHEAD:$AHEAD" &&
|
|
281
|
+
echo "STATUS:$STATUS" &&
|
|
282
|
+
echo "CURRENT:$CURRENT_HASH" &&
|
|
283
|
+
echo "REMOTE:$REMOTE_HASH"
|
|
284
|
+
`.replace(/\n\s+/g, ' ');
|
|
285
|
+
const result = await executeShell(combinedCommand);
|
|
286
|
+
if (!result.success) {
|
|
287
|
+
throw new Error('Failed to check repository status');
|
|
288
|
+
}
|
|
289
|
+
// Parse the output
|
|
290
|
+
const output = result.output;
|
|
291
|
+
const branchMatch = output.match(/BRANCH:([^\n]+)/);
|
|
292
|
+
const behindMatch = output.match(/BEHIND:(\d+)/);
|
|
293
|
+
const aheadMatch = output.match(/AHEAD:(\d+)/);
|
|
294
|
+
const statusMatch = output.match(/STATUS:([\s\S]*?)(?=CURRENT:|$)/);
|
|
295
|
+
const currentMatch = output.match(/CURRENT:([a-f0-9]+)/);
|
|
296
|
+
const remoteMatch = output.match(/REMOTE:([a-f0-9]+|unknown)/);
|
|
297
|
+
const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
|
|
298
|
+
const commitsBehind = behindMatch ? parseInt(behindMatch[1]) : 0;
|
|
299
|
+
const commitsAhead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
|
|
300
|
+
const statusOutput = statusMatch ? statusMatch[1].trim() : '';
|
|
301
|
+
const hasUncommittedChanges = statusOutput.length > 0;
|
|
302
|
+
const currentCommitHash = currentMatch ? currentMatch[1].trim() : 'unknown';
|
|
303
|
+
const remoteCommitHash = remoteMatch ? remoteMatch[1].trim() : 'unknown';
|
|
304
|
+
const isUpToDate = commitsBehind === 0 && commitsAhead === 0;
|
|
305
|
+
const needsPull = commitsBehind > 0;
|
|
306
|
+
const needsPush = commitsAhead > 0;
|
|
307
|
+
logger.info('Repository status checked', {
|
|
308
|
+
repoName,
|
|
309
|
+
commitsBehind,
|
|
310
|
+
commitsAhead,
|
|
311
|
+
hasUncommittedChanges,
|
|
312
|
+
isUpToDate
|
|
313
|
+
});
|
|
314
|
+
// Check for merge conflicts
|
|
315
|
+
const mergeConflicts = await this.getMergeConflicts(socket, repoName);
|
|
316
|
+
return {
|
|
317
|
+
repoName,
|
|
318
|
+
branchName: currentBranch,
|
|
319
|
+
currentCommitHash,
|
|
320
|
+
remoteCommitHash,
|
|
321
|
+
commitsBehind,
|
|
322
|
+
commitsAhead,
|
|
323
|
+
hasUncommittedChanges,
|
|
324
|
+
isUpToDate,
|
|
325
|
+
needsPull,
|
|
326
|
+
needsPush,
|
|
327
|
+
mergeConflicts,
|
|
328
|
+
status: isUpToDate ? 'up-to-date' : (needsPull && needsPush ? 'diverged' : needsPull ? 'behind' : 'ahead')
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get repository history
|
|
336
|
+
*/
|
|
337
|
+
async getRepositoryHistory(repoName, remoteBranchName, limit = 50) {
|
|
338
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
339
|
+
try {
|
|
340
|
+
logger.info('Fetching repository history', { repoName, remoteBranchName, limit });
|
|
341
|
+
const safeLimit = Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 500) : 50;
|
|
342
|
+
let targetBranch = (remoteBranchName || '').trim();
|
|
343
|
+
if (!targetBranch) {
|
|
344
|
+
const branchResult = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
|
|
345
|
+
if (!branchResult.success) {
|
|
346
|
+
throw new Error(`Failed to resolve current branch: ${branchResult.error || branchResult.output || 'Unknown error'}`);
|
|
347
|
+
}
|
|
348
|
+
targetBranch = branchResult.output.trim();
|
|
349
|
+
}
|
|
350
|
+
const resolvedBranch = await this.resolveBranchReference(repoPath, repoName, targetBranch);
|
|
351
|
+
const safeResolvedRef = this.escapeSingleQuotedShell(resolvedBranch.resolvedRef);
|
|
352
|
+
// Get reflog with detailed information
|
|
353
|
+
let result = await executeShell(`cd ${repoPath} && sudo git reflog -${safeLimit} --date=iso`);
|
|
354
|
+
if (!result.success) {
|
|
355
|
+
throw new Error(`Failed to fetch reflog: ${result.error}`);
|
|
356
|
+
}
|
|
357
|
+
const reflogEntries = result.output.trim().split('\n').filter((line) => line.trim());
|
|
358
|
+
// Get commit log with detailed information
|
|
359
|
+
result = await executeShell(`cd ${repoPath} && sudo git log '${safeResolvedRef}' -${safeLimit} --pretty=format:"%H|%an|%ae|%ad|%s" --date=iso`);
|
|
360
|
+
if (!result.success) {
|
|
361
|
+
throw new Error(`Failed to fetch commit log for branch '${resolvedBranch.displayBranch}': ${result.error || result.output || 'Unknown error'}`);
|
|
362
|
+
}
|
|
363
|
+
const commitLog = result.output.trim() ? result.output.trim().split('\n').filter((line) => line.trim()) : [];
|
|
364
|
+
// Parse reflog entries
|
|
365
|
+
const parsedReflog = reflogEntries.map((entry) => {
|
|
366
|
+
// Format: hash HEAD@{n}: action: message
|
|
367
|
+
const match = entry.match(/^([a-f0-9]+)\s+HEAD@\{(\d+)\}:\s+(.+?):\s+(.+)$/);
|
|
368
|
+
if (match) {
|
|
369
|
+
const [, hash, index, action, message] = match;
|
|
370
|
+
// Determine action type
|
|
371
|
+
let actionType = 'unknown';
|
|
372
|
+
let actionDetails = {};
|
|
373
|
+
if (action.includes('commit')) {
|
|
374
|
+
actionType = 'commit';
|
|
375
|
+
actionDetails = { type: action.includes('initial') ? 'initial' : 'regular' };
|
|
376
|
+
}
|
|
377
|
+
else if (action.includes('pull')) {
|
|
378
|
+
actionType = 'pull';
|
|
379
|
+
const branchMatch = message.match(/from (.+)/);
|
|
380
|
+
actionDetails = { source: branchMatch ? branchMatch[1] : 'unknown' };
|
|
381
|
+
}
|
|
382
|
+
else if (action.includes('merge')) {
|
|
383
|
+
actionType = 'merge';
|
|
384
|
+
actionDetails = { message: message };
|
|
385
|
+
}
|
|
386
|
+
else if (action.includes('checkout')) {
|
|
387
|
+
actionType = 'checkout';
|
|
388
|
+
const branchMatch = message.match(/to (.+)/);
|
|
389
|
+
actionDetails = { branch: branchMatch ? branchMatch[1] : message };
|
|
390
|
+
}
|
|
391
|
+
else if (action.includes('rebase')) {
|
|
392
|
+
actionType = 'rebase';
|
|
393
|
+
}
|
|
394
|
+
else if (action.includes('reset')) {
|
|
395
|
+
actionType = 'reset';
|
|
396
|
+
actionDetails = { target: message };
|
|
397
|
+
}
|
|
398
|
+
else if (action.includes('clone')) {
|
|
399
|
+
actionType = 'clone';
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
hash: hash.substring(0, 7),
|
|
403
|
+
fullHash: hash,
|
|
404
|
+
index: parseInt(index),
|
|
405
|
+
action: actionType,
|
|
406
|
+
actionRaw: action,
|
|
407
|
+
message: message,
|
|
408
|
+
details: actionDetails
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}).filter((entry) => entry !== null);
|
|
413
|
+
// Parse commit log
|
|
414
|
+
const parsedCommits = commitLog.map((commit) => {
|
|
415
|
+
const [hash, author, email, date, message] = commit.split('|');
|
|
416
|
+
return {
|
|
417
|
+
hash: hash.substring(0, 7),
|
|
418
|
+
fullHash: hash,
|
|
419
|
+
author,
|
|
420
|
+
email,
|
|
421
|
+
date,
|
|
422
|
+
message
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
const currentBranch = resolvedBranch.displayBranch;
|
|
426
|
+
// Get latest commit info
|
|
427
|
+
result = await executeShell(`cd ${repoPath} && sudo git rev-parse '${safeResolvedRef}'`);
|
|
428
|
+
const latestCommitHash = result.success ? result.output.trim() : 'unknown';
|
|
429
|
+
// Get repository statistics
|
|
430
|
+
result = await executeShell(`cd ${repoPath} && sudo git rev-list --count '${safeResolvedRef}'`);
|
|
431
|
+
const totalCommits = result.success ? parseInt(result.output.trim()) : 0;
|
|
432
|
+
logger.success('Repository history fetched successfully', {
|
|
433
|
+
repoName,
|
|
434
|
+
branchName: resolvedBranch.displayBranch,
|
|
435
|
+
reflogEntries: parsedReflog.length,
|
|
436
|
+
commits: parsedCommits.length
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
repoName,
|
|
440
|
+
currentBranch,
|
|
441
|
+
latestCommitHash,
|
|
442
|
+
totalCommits,
|
|
443
|
+
reflog: parsedReflog,
|
|
444
|
+
commits: parsedCommits,
|
|
445
|
+
summary: {
|
|
446
|
+
totalReflogEntries: parsedReflog.length,
|
|
447
|
+
totalCommitsShown: parsedCommits.length,
|
|
448
|
+
actionTypes: {
|
|
449
|
+
commits: parsedReflog.filter((e) => e.action === 'commit').length,
|
|
450
|
+
pulls: parsedReflog.filter((e) => e.action === 'pull').length,
|
|
451
|
+
merges: parsedReflog.filter((e) => e.action === 'merge').length,
|
|
452
|
+
checkouts: parsedReflog.filter((e) => e.action === 'checkout').length,
|
|
453
|
+
resets: parsedReflog.filter((e) => e.action === 'reset').length,
|
|
454
|
+
rebases: parsedReflog.filter((e) => e.action === 'rebase').length
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Commit local changes
|
|
464
|
+
*/
|
|
465
|
+
async commitLocalChanges(repoName, remoteBranchName, commitMessage, userInfo, socket, userId) {
|
|
466
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
467
|
+
let commitMsg = commitMessage;
|
|
468
|
+
if (!commitMessage) {
|
|
469
|
+
commitMsg = 'Auto-commit by AI-Playgrounds Bot';
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
logger.info('Starting local commit operation', { repoName, remoteBranchName, commitMessage });
|
|
473
|
+
// Escape commit message for shell safety
|
|
474
|
+
const commitAuthorName = (userInfo === null || userInfo === void 0 ? void 0 : userInfo.name) || 'Phantom';
|
|
475
|
+
const commitAuthorEmail = (userInfo === null || userInfo === void 0 ? void 0 : userInfo.email) || 'noreply@ai-playgrounds.com';
|
|
476
|
+
const escapedCommitMsg = commitMsg.replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
477
|
+
const escapedAuthorName = commitAuthorName.replace(/"/g, '\\"');
|
|
478
|
+
const escapedAuthorEmail = commitAuthorEmail.replace(/"/g, '\\"');
|
|
479
|
+
// Combine all commands into a single SSH call
|
|
480
|
+
const combinedCommand = `
|
|
481
|
+
cd ${repoPath} &&
|
|
482
|
+
sudo git config user.name "${escapedAuthorName}" &&
|
|
483
|
+
sudo git config user.email "${escapedAuthorEmail}" &&
|
|
484
|
+
STATUS=$(sudo git status --porcelain) &&
|
|
485
|
+
if [ -z "$STATUS" ]; then
|
|
486
|
+
echo "ERROR:NO_CHANGES";
|
|
487
|
+
exit 1;
|
|
488
|
+
fi &&
|
|
489
|
+
CHANGED_FILES=$(echo "$STATUS" | wc -l) &&
|
|
490
|
+
sudo git add . &&
|
|
491
|
+
sudo git commit -m "${escapedCommitMsg}" &&
|
|
492
|
+
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
493
|
+
SHORT_HASH=$(sudo git rev-parse --short HEAD) &&
|
|
494
|
+
echo "CHANGED_FILES:$CHANGED_FILES" &&
|
|
495
|
+
echo "COMMIT_HASH:$COMMIT_HASH" &&
|
|
496
|
+
echo "SHORT_HASH:$SHORT_HASH"
|
|
497
|
+
`.replace(/\n\s+/g, ' ');
|
|
498
|
+
const result = await executeShell(combinedCommand);
|
|
499
|
+
if (!result.success) {
|
|
500
|
+
if (result.output && result.output.includes('ERROR:NO_CHANGES')) {
|
|
501
|
+
throw new Error('No changes to commit');
|
|
502
|
+
}
|
|
503
|
+
throw new Error('Failed to commit local changes');
|
|
504
|
+
}
|
|
505
|
+
// Parse output
|
|
506
|
+
const output = result.output;
|
|
507
|
+
const changedFilesMatch = output.match(/CHANGED_FILES:(\d+)/);
|
|
508
|
+
const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
|
|
509
|
+
const shortHashMatch = output.match(/SHORT_HASH:([a-f0-9]+)/);
|
|
510
|
+
const changedFiles = changedFilesMatch ? parseInt(changedFilesMatch[1]) : 0;
|
|
511
|
+
const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
|
|
512
|
+
const shortHash = shortHashMatch ? shortHashMatch[1].trim() : commitHash.substring(0, 7);
|
|
513
|
+
logger.success('Local commit completed successfully', { repoName, commitHash, changedFiles });
|
|
514
|
+
const resultData = {
|
|
515
|
+
repoName,
|
|
516
|
+
branchName: remoteBranchName,
|
|
517
|
+
commitMessage,
|
|
518
|
+
commitHash,
|
|
519
|
+
shortHash,
|
|
520
|
+
changedFiles,
|
|
521
|
+
author: commitAuthorName,
|
|
522
|
+
email: commitAuthorEmail,
|
|
523
|
+
committedAt: new Date().toISOString()
|
|
524
|
+
};
|
|
525
|
+
// Emit socket event to refresh repo on client (only when called by AI agent)
|
|
526
|
+
if (socket) {
|
|
527
|
+
socket.emit('refresh_repo', { repoName });
|
|
528
|
+
}
|
|
529
|
+
// Update database asynchronously (non-blocking)
|
|
530
|
+
// if (userId) {
|
|
531
|
+
// (async () => {
|
|
532
|
+
// try {
|
|
533
|
+
// const dbService = await getDBService();
|
|
534
|
+
// const dbName = ds.UserInfo.get(userId)?.dbName;
|
|
535
|
+
// if (!dbName) throw new Error('User dbName not found');
|
|
536
|
+
// const taskRepository = dbService.getRepository(dbName, CollectionNames.TASKS);
|
|
537
|
+
// const task = await taskRepository.findOne();
|
|
538
|
+
// let gitChanges = (task?.metadata?.github) || {
|
|
539
|
+
// totalAdditions: 0,
|
|
540
|
+
// totalDeletions: 0,
|
|
541
|
+
// totalChangedFiles: 0,
|
|
542
|
+
// pullRequests: [],
|
|
543
|
+
// commits: []
|
|
544
|
+
// };
|
|
545
|
+
// const commitObj = {
|
|
546
|
+
// id: shortHash,
|
|
547
|
+
// sha: commitHash,
|
|
548
|
+
// message: commitMessage,
|
|
549
|
+
// author: commitAuthorEmail,
|
|
550
|
+
// authorName: commitAuthorName,
|
|
551
|
+
// timestamp: resultData.committedAt,
|
|
552
|
+
// url: undefined,
|
|
553
|
+
// additions: changedFiles,
|
|
554
|
+
// deletions: 0,
|
|
555
|
+
// changedFiles: changedFiles,
|
|
556
|
+
// files: []
|
|
557
|
+
// };
|
|
558
|
+
// gitChanges.commits.push(commitObj);
|
|
559
|
+
// gitChanges.totalChangedFiles += changedFiles;
|
|
560
|
+
// await taskRepository.updateOne(, { $set: { 'metadata.github': gitChanges } });
|
|
561
|
+
// } catch (err) {
|
|
562
|
+
// logger.error('Failed to update task metadata.github for commit', { err });
|
|
563
|
+
// }
|
|
564
|
+
// })();
|
|
565
|
+
// }
|
|
566
|
+
return resultData;
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Create pull request
|
|
573
|
+
*/
|
|
574
|
+
async createPullRequest(repoName, targetBranch, title, body, socket) {
|
|
575
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
576
|
+
const installationToken = await fetchInstallationToken();
|
|
577
|
+
const gitOrgName = await getGithubOrganizationName();
|
|
578
|
+
try {
|
|
579
|
+
logger.info('Creating pull request', { repoName, targetBranch });
|
|
580
|
+
// Combine commands into a single SSH call, with remote URL set/unset
|
|
581
|
+
const combinedCommand = `
|
|
582
|
+
cd ${repoPath} &&
|
|
583
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
584
|
+
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
585
|
+
sudo git fetch origin ${targetBranch} 2>/dev/null &&
|
|
586
|
+
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
587
|
+
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
588
|
+
echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
|
|
589
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
|
|
590
|
+
exit 1;
|
|
591
|
+
fi &&
|
|
592
|
+
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
593
|
+
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
594
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
|
|
595
|
+
`.replace(/\n\s+/g, ' ');
|
|
596
|
+
let result = await executeShell(combinedCommand);
|
|
597
|
+
const output = result.output || '';
|
|
598
|
+
const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
|
|
599
|
+
const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
|
|
600
|
+
const errorMatch = output.match(/ERROR:NO_COMMITS:([^\n]+)/);
|
|
601
|
+
let headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
|
|
602
|
+
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
603
|
+
if (!result.success && errorMatch) {
|
|
604
|
+
headBranch = errorMatch[1].trim();
|
|
605
|
+
}
|
|
606
|
+
const { Octokit } = require('@octokit/rest');
|
|
607
|
+
const octokit = new Octokit({
|
|
608
|
+
auth: installationToken
|
|
609
|
+
});
|
|
610
|
+
// Always check for existing PR first even if there are no new commits
|
|
611
|
+
try {
|
|
612
|
+
const existingPRs = await octokit.pulls.list({
|
|
613
|
+
owner: gitOrgName,
|
|
614
|
+
repo: repoName,
|
|
615
|
+
head: `${gitOrgName}:${headBranch}`,
|
|
616
|
+
base: targetBranch,
|
|
617
|
+
state: 'open'
|
|
618
|
+
});
|
|
619
|
+
if (existingPRs.data.length > 0) {
|
|
620
|
+
const existingPR = existingPRs.data[0];
|
|
621
|
+
logger.info('Pull request already exists', {
|
|
622
|
+
repoName,
|
|
623
|
+
prNumber: existingPR.number,
|
|
624
|
+
prUrl: existingPR.html_url
|
|
625
|
+
});
|
|
626
|
+
const resultData = {
|
|
627
|
+
repoName,
|
|
628
|
+
headBranch,
|
|
629
|
+
targetBranch,
|
|
630
|
+
prNumber: existingPR.number,
|
|
631
|
+
prUrl: existingPR.html_url,
|
|
632
|
+
prTitle: existingPR.title,
|
|
633
|
+
commitsAhead,
|
|
634
|
+
createdAt: existingPR.created_at,
|
|
635
|
+
alreadyExists: true
|
|
636
|
+
};
|
|
637
|
+
// Emit socket event to refresh PR status on client
|
|
638
|
+
if (socket) {
|
|
639
|
+
socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
|
|
640
|
+
}
|
|
641
|
+
// the socket server our main server will automatically update the pr metadata in db when i recvs the result of the given tool.
|
|
642
|
+
// this.updatePRMetadata(userId, resultData);
|
|
643
|
+
return resultData;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (fetchError) {
|
|
647
|
+
logger.error('Failed to check for existing PR', fetchError);
|
|
648
|
+
}
|
|
649
|
+
// No existing PR found - check if we have commits to create one
|
|
650
|
+
if (!result.success) {
|
|
651
|
+
if (errorMatch) {
|
|
652
|
+
throw new Error(`No commits to create PR. Branch '${headBranch}' is not ahead of '${targetBranch}'`);
|
|
653
|
+
}
|
|
654
|
+
throw new Error('Failed to prepare pull request');
|
|
655
|
+
}
|
|
656
|
+
// Create new PR
|
|
657
|
+
const prTitle = title || `Merge ${headBranch} into ${targetBranch}`;
|
|
658
|
+
const prBody = body || `This pull request merges changes from ${headBranch} into ${targetBranch}.\n\nCreated by AI-Playgrounds`;
|
|
659
|
+
try {
|
|
660
|
+
const prResponse = await octokit.pulls.create({
|
|
661
|
+
owner: gitOrgName,
|
|
662
|
+
repo: repoName,
|
|
663
|
+
title: prTitle,
|
|
664
|
+
body: prBody,
|
|
665
|
+
head: headBranch,
|
|
666
|
+
base: targetBranch
|
|
667
|
+
});
|
|
668
|
+
const prUrl = prResponse.data.html_url;
|
|
669
|
+
const prNumber = prResponse.data.number;
|
|
670
|
+
logger.success('Pull request created successfully', {
|
|
671
|
+
repoName,
|
|
672
|
+
prNumber,
|
|
673
|
+
prUrl
|
|
674
|
+
});
|
|
675
|
+
// taskInfo.status = TaskStatus.Completed;
|
|
676
|
+
// taskInfo.nonRunningSince = new Date();
|
|
677
|
+
// //updating the task status in the db
|
|
678
|
+
// let dbService = await getDBService();
|
|
679
|
+
// let taskHanlder = dbService.getRepository<Task>(ds.UserInfo.get(userId as any)?.dbName, CollectionNames.TASKS);
|
|
680
|
+
// taskHanlder.updateOne({
|
|
681
|
+
// taskId: taskId
|
|
682
|
+
// },
|
|
683
|
+
// {
|
|
684
|
+
// "$set": {
|
|
685
|
+
// status: TaskStatus.Completed
|
|
686
|
+
// }
|
|
687
|
+
// });
|
|
688
|
+
const resultData = {
|
|
689
|
+
repoName,
|
|
690
|
+
headBranch,
|
|
691
|
+
targetBranch,
|
|
692
|
+
prNumber,
|
|
693
|
+
prUrl,
|
|
694
|
+
prTitle,
|
|
695
|
+
commitsAhead,
|
|
696
|
+
createdAt: new Date().toISOString()
|
|
697
|
+
};
|
|
698
|
+
// // Emit socket event to refresh PR status on client
|
|
699
|
+
// if (socket) {
|
|
700
|
+
// socket.emit('refresh_pr_status', { repoName, prNumber });
|
|
701
|
+
// }
|
|
702
|
+
// // Update database asynchronously (non-blocking)
|
|
703
|
+
// this.updatePRMetadata(userId, resultData);
|
|
704
|
+
return resultData;
|
|
705
|
+
}
|
|
706
|
+
catch (prError) {
|
|
707
|
+
// Check if PR already exists
|
|
708
|
+
if (prError.status === 422) {
|
|
709
|
+
// Fetch existing PR
|
|
710
|
+
try {
|
|
711
|
+
const existingPRs = await octokit.pulls.list({
|
|
712
|
+
owner: gitOrgName,
|
|
713
|
+
repo: repoName,
|
|
714
|
+
head: `${gitOrgName}:${headBranch}`,
|
|
715
|
+
base: targetBranch,
|
|
716
|
+
state: 'open'
|
|
717
|
+
});
|
|
718
|
+
if (existingPRs.data.length > 0) {
|
|
719
|
+
const existingPR = existingPRs.data[0];
|
|
720
|
+
logger.info('Pull request already exists', {
|
|
721
|
+
repoName,
|
|
722
|
+
prNumber: existingPR.number,
|
|
723
|
+
prUrl: existingPR.html_url
|
|
724
|
+
});
|
|
725
|
+
const resultData = {
|
|
726
|
+
repoName,
|
|
727
|
+
headBranch,
|
|
728
|
+
targetBranch,
|
|
729
|
+
prNumber: existingPR.number,
|
|
730
|
+
prUrl: existingPR.html_url,
|
|
731
|
+
prTitle: existingPR.title,
|
|
732
|
+
commitsAhead,
|
|
733
|
+
createdAt: existingPR.created_at,
|
|
734
|
+
alreadyExists: true
|
|
735
|
+
};
|
|
736
|
+
// Emit socket event to refresh PR status on client
|
|
737
|
+
if (socket) {
|
|
738
|
+
socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
|
|
739
|
+
}
|
|
740
|
+
// Update database asynchronously (non-blocking)
|
|
741
|
+
// this.updatePRMetadata(userId, resultData);
|
|
742
|
+
return resultData;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch (fetchError) {
|
|
746
|
+
logger.error('Failed to fetch existing PR', fetchError);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
throw prError;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Check if pull request exists
|
|
757
|
+
*/
|
|
758
|
+
async mergeBranchIntoBranch(repoName, sourceBranch, targetBranch, pushToRemote = false, socket) {
|
|
759
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
760
|
+
const installationToken = await fetchInstallationToken();
|
|
761
|
+
const gitOrgName = await getGithubOrganizationName();
|
|
762
|
+
try {
|
|
763
|
+
const trimmedSourceBranch = (sourceBranch || '').trim();
|
|
764
|
+
const trimmedTargetBranch = (targetBranch || '').trim();
|
|
765
|
+
if (!trimmedSourceBranch || !trimmedTargetBranch) {
|
|
766
|
+
throw new Error('Both sourceBranch and targetBranch are required');
|
|
767
|
+
}
|
|
768
|
+
if (trimmedSourceBranch === trimmedTargetBranch) {
|
|
769
|
+
throw new Error('sourceBranch and targetBranch cannot be the same');
|
|
770
|
+
}
|
|
771
|
+
logger.info('Starting branch merge operation', {
|
|
772
|
+
repoName,
|
|
773
|
+
sourceBranch: trimmedSourceBranch,
|
|
774
|
+
targetBranch: trimmedTargetBranch,
|
|
775
|
+
pushToRemote
|
|
776
|
+
});
|
|
777
|
+
let result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
|
|
778
|
+
if (!result.success) {
|
|
779
|
+
throw new Error(`Failed to set authenticated remote URL: ${result.error || result.output || 'Unknown error'}`);
|
|
780
|
+
}
|
|
781
|
+
const fetchResult = await executeShell(`cd ${repoPath} && sudo git fetch origin --prune`);
|
|
782
|
+
if (!fetchResult.success) {
|
|
783
|
+
throw new Error(`Failed to fetch remote branches: ${fetchResult.error || fetchResult.output || 'Unknown error'}`);
|
|
784
|
+
}
|
|
785
|
+
const resolvedSource = await this.resolveBranchReference(repoPath, repoName, trimmedSourceBranch);
|
|
786
|
+
const resolvedTarget = await this.resolveBranchReference(repoPath, repoName, trimmedTargetBranch);
|
|
787
|
+
const targetLocalBranch = resolvedTarget.displayBranch;
|
|
788
|
+
const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
|
|
789
|
+
const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
|
|
790
|
+
const prepareBranchCommand = `
|
|
791
|
+
cd ${repoPath} &&
|
|
792
|
+
TARGET_LOCAL='${safeTargetLocalBranch}' &&
|
|
793
|
+
if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
|
|
794
|
+
sudo git checkout "$TARGET_LOCAL";
|
|
795
|
+
elif sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
796
|
+
sudo git checkout -b "$TARGET_LOCAL" "origin/$TARGET_LOCAL";
|
|
797
|
+
else
|
|
798
|
+
echo "ERROR:TARGET_BRANCH_NOT_CHECKOUTABLE";
|
|
799
|
+
exit 1;
|
|
800
|
+
fi &&
|
|
801
|
+
if sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
802
|
+
sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
|
|
803
|
+
fi
|
|
804
|
+
`.replace(/\n\s+/g, ' ');
|
|
805
|
+
result = await executeShell(prepareBranchCommand);
|
|
806
|
+
if (!result.success) {
|
|
807
|
+
throw new Error(`Failed to prepare target branch for merge: ${result.error || result.output || 'Unknown error'}`);
|
|
808
|
+
}
|
|
809
|
+
const mergeCommand = `cd ${repoPath} && SOURCE_REF='${safeSourceRef}' && sudo git merge --no-ff --no-edit "$SOURCE_REF"`;
|
|
810
|
+
const mergeResult = await executeShell(mergeCommand);
|
|
811
|
+
if (!mergeResult.success) {
|
|
812
|
+
const conflictResult = await executeShell(`cd ${repoPath} && sudo git diff --name-only --diff-filter=U`);
|
|
813
|
+
const conflictFiles = (conflictResult.output || '')
|
|
814
|
+
.split('\n')
|
|
815
|
+
.map((line) => line.trim())
|
|
816
|
+
.filter((line) => line.length > 0);
|
|
817
|
+
if (conflictFiles.length > 0) {
|
|
818
|
+
logger.warn('Merge completed with conflicts', {
|
|
819
|
+
repoName,
|
|
820
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
821
|
+
targetBranch: targetLocalBranch,
|
|
822
|
+
conflictFiles
|
|
823
|
+
});
|
|
824
|
+
return {
|
|
825
|
+
repoName,
|
|
826
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
827
|
+
targetBranch: targetLocalBranch,
|
|
828
|
+
merged: false,
|
|
829
|
+
hasConflicts: true,
|
|
830
|
+
conflictFiles,
|
|
831
|
+
currentBranch: targetLocalBranch,
|
|
832
|
+
pushedToRemote: false,
|
|
833
|
+
mergedAt: new Date().toISOString(),
|
|
834
|
+
message: `Merge has conflicts. Resolve conflicts in target branch '${targetLocalBranch}'.`
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
throw new Error(`Failed to merge branches: ${mergeResult.error || mergeResult.output || 'Unknown error'}`);
|
|
838
|
+
}
|
|
839
|
+
const summaryResult = await executeShell(`cd ${repoPath} && COMMIT_HASH=$(sudo git rev-parse HEAD) && CURRENT_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) && echo "COMMIT_HASH:$COMMIT_HASH" && echo "CURRENT_BRANCH:$CURRENT_BRANCH"`);
|
|
840
|
+
if (!summaryResult.success) {
|
|
841
|
+
throw new Error(`Merge succeeded but failed to fetch merge summary: ${summaryResult.error || summaryResult.output || 'Unknown error'}`);
|
|
842
|
+
}
|
|
843
|
+
const summaryOutput = summaryResult.output || '';
|
|
844
|
+
const commitHashMatch = summaryOutput.match(/COMMIT_HASH:([^\n]+)/);
|
|
845
|
+
const currentBranchMatch = summaryOutput.match(/CURRENT_BRANCH:([^\n]+)/);
|
|
846
|
+
const commitHash = commitHashMatch ? commitHashMatch[1].trim() : '';
|
|
847
|
+
const currentBranch = currentBranchMatch ? currentBranchMatch[1].trim() : targetLocalBranch;
|
|
848
|
+
let pushedToRemote = false;
|
|
849
|
+
if (pushToRemote) {
|
|
850
|
+
const pushResult = await executeShell(`cd ${repoPath} && sudo git push origin '${safeTargetLocalBranch}'`);
|
|
851
|
+
if (!pushResult.success) {
|
|
852
|
+
throw new Error(`Merge succeeded but push failed: ${pushResult.error || pushResult.output || 'Unknown error'}`);
|
|
853
|
+
}
|
|
854
|
+
pushedToRemote = true;
|
|
855
|
+
}
|
|
856
|
+
logger.success('Branch merge completed successfully', {
|
|
857
|
+
repoName,
|
|
858
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
859
|
+
targetBranch: targetLocalBranch,
|
|
860
|
+
commitHash,
|
|
861
|
+
pushedToRemote
|
|
862
|
+
});
|
|
863
|
+
if (socket) {
|
|
864
|
+
socket.emit('refresh_repo', { repoName });
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
repoName,
|
|
868
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
869
|
+
targetBranch: targetLocalBranch,
|
|
870
|
+
merged: true,
|
|
871
|
+
hasConflicts: false,
|
|
872
|
+
conflictFiles: [],
|
|
873
|
+
currentBranch,
|
|
874
|
+
commitHash,
|
|
875
|
+
pushedToRemote,
|
|
876
|
+
mergedAt: new Date().toISOString(),
|
|
877
|
+
message: pushedToRemote
|
|
878
|
+
? `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' and pushed to remote.`
|
|
879
|
+
: `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' locally.`
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
finally {
|
|
883
|
+
try {
|
|
884
|
+
await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
|
|
885
|
+
}
|
|
886
|
+
catch (restoreError) {
|
|
887
|
+
logger.error('Failed to restore unauthenticated remote URL after merge operation', {
|
|
888
|
+
repoName,
|
|
889
|
+
error: restoreError instanceof Error ? restoreError.message : String(restoreError)
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async checkPullRequestExists(repoName, targetBranch) {
|
|
895
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
896
|
+
const installationToken = await fetchInstallationToken();
|
|
897
|
+
const gitOrgName = await getGithubOrganizationName();
|
|
898
|
+
try {
|
|
899
|
+
logger.info('Checking if PR exists', { repoName, targetBranch });
|
|
900
|
+
// Get current branch and commits ahead
|
|
901
|
+
const combinedCommand = `
|
|
902
|
+
cd ${repoPath} &&
|
|
903
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
904
|
+
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
905
|
+
sudo git fetch origin ${targetBranch} &&
|
|
906
|
+
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
907
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
908
|
+
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
909
|
+
echo "COMMITS_AHEAD:$COMMITS_AHEAD"
|
|
910
|
+
`.replace(/\n\s+/g, ' ');
|
|
911
|
+
const result = await executeShell(combinedCommand);
|
|
912
|
+
// Check for command execution failure
|
|
913
|
+
if (!result.success) {
|
|
914
|
+
logger.error('Failed to check PR status', {
|
|
915
|
+
repoName,
|
|
916
|
+
error: result.error,
|
|
917
|
+
code: result.code,
|
|
918
|
+
output: result.output
|
|
919
|
+
});
|
|
920
|
+
throw new Error(`Failed to check PR status: ${result.error}`);
|
|
921
|
+
}
|
|
922
|
+
const output = result.output || '';
|
|
923
|
+
const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
|
|
924
|
+
const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
|
|
925
|
+
const headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
|
|
926
|
+
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
927
|
+
logger.info('Branch info retrieved', { headBranch, commitsAhead });
|
|
928
|
+
// Check for existing PR
|
|
929
|
+
const { Octokit } = require('@octokit/rest');
|
|
930
|
+
const octokit = new Octokit({
|
|
931
|
+
auth: installationToken
|
|
932
|
+
});
|
|
933
|
+
const existingPRs = await octokit.pulls.list({
|
|
934
|
+
owner: gitOrgName,
|
|
935
|
+
repo: repoName,
|
|
936
|
+
head: `${gitOrgName}:${headBranch}`,
|
|
937
|
+
base: targetBranch,
|
|
938
|
+
state: 'open'
|
|
939
|
+
});
|
|
940
|
+
if (existingPRs.data.length > 0) {
|
|
941
|
+
const existingPR = existingPRs.data[0];
|
|
942
|
+
logger.info('Pull request exists', {
|
|
943
|
+
repoName,
|
|
944
|
+
prNumber: existingPR.number,
|
|
945
|
+
prUrl: existingPR.html_url
|
|
946
|
+
});
|
|
947
|
+
return {
|
|
948
|
+
repoName,
|
|
949
|
+
headBranch,
|
|
950
|
+
targetBranch,
|
|
951
|
+
prExists: true,
|
|
952
|
+
prNumber: existingPR.number,
|
|
953
|
+
prUrl: existingPR.html_url,
|
|
954
|
+
prTitle: existingPR.title,
|
|
955
|
+
commitsAhead,
|
|
956
|
+
createdAt: existingPR.created_at
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
logger.info('No pull request found', { repoName, headBranch, targetBranch });
|
|
961
|
+
return {
|
|
962
|
+
repoName,
|
|
963
|
+
headBranch,
|
|
964
|
+
targetBranch,
|
|
965
|
+
prExists: false,
|
|
966
|
+
commitsAhead
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
finally {
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Get commit list with commit IDs for a branch
|
|
975
|
+
*/
|
|
976
|
+
async getCommitList(repoName, branchName, limit = 100) {
|
|
977
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
978
|
+
try {
|
|
979
|
+
logger.info('Fetching commit list', { repoName, branchName, limit });
|
|
980
|
+
const safeLimit = Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 500) : 100;
|
|
981
|
+
let targetBranch = (branchName || '').trim();
|
|
982
|
+
if (!targetBranch) {
|
|
983
|
+
const branchResult = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
|
|
984
|
+
if (!branchResult.success) {
|
|
985
|
+
throw new Error(`Failed to resolve current branch: ${branchResult.error || branchResult.output || 'Unknown error'}`);
|
|
986
|
+
}
|
|
987
|
+
targetBranch = branchResult.output.trim();
|
|
988
|
+
}
|
|
989
|
+
const resolvedBranch = await this.resolveBranchReference(repoPath, repoName, targetBranch);
|
|
990
|
+
const safeResolvedRef = this.escapeSingleQuotedShell(resolvedBranch.resolvedRef);
|
|
991
|
+
const logResult = await executeShell(`cd ${repoPath} && sudo git log '${safeResolvedRef}' -${safeLimit} --pretty=format:"%H%x1f%h%x1f%an%x1f%ae%x1f%ad%x1f%s" --date=iso`);
|
|
992
|
+
if (!logResult.success) {
|
|
993
|
+
throw new Error(`Failed to fetch commit list: ${logResult.error || logResult.output || 'Unknown error'}`);
|
|
994
|
+
}
|
|
995
|
+
const commitLines = (logResult.output || '').trim()
|
|
996
|
+
? logResult.output.trim().split('\n').filter((line) => line.trim())
|
|
997
|
+
: [];
|
|
998
|
+
const commits = commitLines
|
|
999
|
+
.map((line) => {
|
|
1000
|
+
const [fullHash, hash, author, email, date, ...messageParts] = line.split('\x1f');
|
|
1001
|
+
return {
|
|
1002
|
+
hash: (hash || '').trim(),
|
|
1003
|
+
fullHash: (fullHash || '').trim(),
|
|
1004
|
+
author: (author || '').trim(),
|
|
1005
|
+
email: (email || '').trim(),
|
|
1006
|
+
date: (date || '').trim(),
|
|
1007
|
+
message: messageParts.join('\x1f').trim()
|
|
1008
|
+
};
|
|
1009
|
+
})
|
|
1010
|
+
.filter((commit) => commit.fullHash);
|
|
1011
|
+
logger.success('Commit list fetched successfully', {
|
|
1012
|
+
repoName,
|
|
1013
|
+
branchName: resolvedBranch.displayBranch,
|
|
1014
|
+
commits: commits.length
|
|
1015
|
+
});
|
|
1016
|
+
return {
|
|
1017
|
+
repoName,
|
|
1018
|
+
branchName: resolvedBranch.displayBranch,
|
|
1019
|
+
totalCommits: commits.length,
|
|
1020
|
+
commits
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
finally {
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get latest commit diff for a given branch
|
|
1028
|
+
*/
|
|
1029
|
+
async getLatestCommitDiff(repoName, branchName) {
|
|
1030
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
1031
|
+
try {
|
|
1032
|
+
logger.info('Fetching latest commit diff', { repoName, branchName });
|
|
1033
|
+
let targetBranch = (branchName || '').trim();
|
|
1034
|
+
if (!targetBranch) {
|
|
1035
|
+
const branchResult = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
|
|
1036
|
+
if (!branchResult.success) {
|
|
1037
|
+
throw new Error(`Failed to resolve current branch: ${branchResult.error || branchResult.output || 'Unknown error'}`);
|
|
1038
|
+
}
|
|
1039
|
+
targetBranch = branchResult.output.trim();
|
|
1040
|
+
}
|
|
1041
|
+
const resolvedBranch = await this.resolveBranchReference(repoPath, repoName, targetBranch);
|
|
1042
|
+
const safeResolvedRef = this.escapeSingleQuotedShell(resolvedBranch.resolvedRef);
|
|
1043
|
+
const commitInfoResult = await executeShell(`cd ${repoPath} && sudo git show --no-patch --pretty=format:"%H%x1f%h%x1f%an%x1f%ae%x1f%ad%x1f%s" --date=iso '${safeResolvedRef}'`);
|
|
1044
|
+
if (!commitInfoResult.success) {
|
|
1045
|
+
throw new Error(`Failed to fetch latest commit information: ${commitInfoResult.error || commitInfoResult.output || 'Unknown error'}`);
|
|
1046
|
+
}
|
|
1047
|
+
const commitInfo = (commitInfoResult.output || '').trim();
|
|
1048
|
+
if (!commitInfo) {
|
|
1049
|
+
throw new Error(`No commits found on branch '${resolvedBranch.displayBranch}'`);
|
|
1050
|
+
}
|
|
1051
|
+
const [fullHash, hash, author, email, date, ...messageParts] = commitInfo.split('\x1f');
|
|
1052
|
+
const commitMessage = messageParts.join('\x1f').trim();
|
|
1053
|
+
const safeCommitHash = this.escapeSingleQuotedShell((fullHash || '').trim());
|
|
1054
|
+
const diffResult = await executeShell(`cd ${repoPath} && sudo git show --pretty=format:"" '${safeCommitHash}'`);
|
|
1055
|
+
if (!diffResult.success) {
|
|
1056
|
+
throw new Error(`Failed to fetch diff for commit '${hash}': ${diffResult.error || diffResult.output || 'Unknown error'}`);
|
|
1057
|
+
}
|
|
1058
|
+
const statsResult = await executeShell(`cd ${repoPath} && sudo git show --numstat --pretty="" '${safeCommitHash}'`);
|
|
1059
|
+
if (!statsResult.success) {
|
|
1060
|
+
throw new Error(`Failed to fetch commit stats for '${hash}': ${statsResult.error || statsResult.output || 'Unknown error'}`);
|
|
1061
|
+
}
|
|
1062
|
+
const statLines = (statsResult.output || '').trim()
|
|
1063
|
+
? statsResult.output.trim().split('\n').filter((line) => line.trim())
|
|
1064
|
+
: [];
|
|
1065
|
+
let totalAdditions = 0;
|
|
1066
|
+
let totalDeletions = 0;
|
|
1067
|
+
for (const line of statLines) {
|
|
1068
|
+
const [additionsRaw, deletionsRaw] = line.split('\t');
|
|
1069
|
+
const additions = additionsRaw === '-' ? 0 : parseInt(additionsRaw, 10) || 0;
|
|
1070
|
+
const deletions = deletionsRaw === '-' ? 0 : parseInt(deletionsRaw, 10) || 0;
|
|
1071
|
+
totalAdditions += additions;
|
|
1072
|
+
totalDeletions += deletions;
|
|
1073
|
+
}
|
|
1074
|
+
logger.success('Latest commit diff fetched successfully', {
|
|
1075
|
+
repoName,
|
|
1076
|
+
branchName: resolvedBranch.displayBranch,
|
|
1077
|
+
commitHash: hash,
|
|
1078
|
+
filesChanged: statLines.length
|
|
1079
|
+
});
|
|
1080
|
+
return {
|
|
1081
|
+
repoName,
|
|
1082
|
+
branchName: resolvedBranch.displayBranch,
|
|
1083
|
+
commit: {
|
|
1084
|
+
hash: (hash || '').trim(),
|
|
1085
|
+
fullHash: (fullHash || '').trim(),
|
|
1086
|
+
author: (author || '').trim(),
|
|
1087
|
+
email: (email || '').trim(),
|
|
1088
|
+
date: (date || '').trim(),
|
|
1089
|
+
message: commitMessage
|
|
1090
|
+
},
|
|
1091
|
+
diff: diffResult.output || '',
|
|
1092
|
+
summary: {
|
|
1093
|
+
filesChanged: statLines.length,
|
|
1094
|
+
totalAdditions,
|
|
1095
|
+
totalDeletions,
|
|
1096
|
+
totalChanges: totalAdditions + totalDeletions
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
finally {
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Get diff for a specific commit hash
|
|
1105
|
+
*/
|
|
1106
|
+
async getCommitDiffByHash(repoName, commitHash) {
|
|
1107
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
1108
|
+
try {
|
|
1109
|
+
const requestedCommit = (commitHash || '').trim();
|
|
1110
|
+
if (!requestedCommit) {
|
|
1111
|
+
throw new Error('commitHash is required');
|
|
1112
|
+
}
|
|
1113
|
+
logger.info('Fetching commit diff by hash', { repoName, commitHash: requestedCommit });
|
|
1114
|
+
const sanitizedCommitHash = requestedCommit.replace(/'/g, `'\\''`);
|
|
1115
|
+
const commitCheck = await executeShell(`cd ${repoPath} && sudo git rev-parse --verify '${sanitizedCommitHash}^{commit}' >/dev/null 2>&1 && echo "COMMIT_OK"`);
|
|
1116
|
+
if (!commitCheck.success || !(commitCheck.output || '').includes('COMMIT_OK')) {
|
|
1117
|
+
throw new Error(`Commit '${requestedCommit}' not found in repository '${repoName}'`);
|
|
1118
|
+
}
|
|
1119
|
+
const commitInfoResult = await executeShell(`cd ${repoPath} && sudo git show --no-patch --pretty=format:"%H%x1f%h%x1f%an%x1f%ae%x1f%ad%x1f%s" --date=iso '${sanitizedCommitHash}'`);
|
|
1120
|
+
if (!commitInfoResult.success) {
|
|
1121
|
+
throw new Error(`Failed to fetch commit information: ${commitInfoResult.error || commitInfoResult.output || 'Unknown error'}`);
|
|
1122
|
+
}
|
|
1123
|
+
const commitInfo = (commitInfoResult.output || '').trim();
|
|
1124
|
+
if (!commitInfo) {
|
|
1125
|
+
throw new Error(`No commit metadata found for '${requestedCommit}'`);
|
|
1126
|
+
}
|
|
1127
|
+
const [fullHash, hash, author, email, date, ...messageParts] = commitInfo.split('\x1f');
|
|
1128
|
+
const commitMessage = messageParts.join('\x1f').trim();
|
|
1129
|
+
const diffResult = await executeShell(`cd ${repoPath} && sudo git show --pretty=format:"" '${sanitizedCommitHash}'`);
|
|
1130
|
+
if (!diffResult.success) {
|
|
1131
|
+
throw new Error(`Failed to fetch diff for commit '${requestedCommit}': ${diffResult.error || diffResult.output || 'Unknown error'}`);
|
|
1132
|
+
}
|
|
1133
|
+
const statsResult = await executeShell(`cd ${repoPath} && sudo git show --numstat --pretty="" '${sanitizedCommitHash}'`);
|
|
1134
|
+
if (!statsResult.success) {
|
|
1135
|
+
throw new Error(`Failed to fetch commit stats for '${requestedCommit}': ${statsResult.error || statsResult.output || 'Unknown error'}`);
|
|
1136
|
+
}
|
|
1137
|
+
const statLines = (statsResult.output || '').trim()
|
|
1138
|
+
? statsResult.output.trim().split('\n').filter((line) => line.trim())
|
|
1139
|
+
: [];
|
|
1140
|
+
let totalAdditions = 0;
|
|
1141
|
+
let totalDeletions = 0;
|
|
1142
|
+
for (const line of statLines) {
|
|
1143
|
+
const [additionsRaw, deletionsRaw] = line.split('\t');
|
|
1144
|
+
const additions = additionsRaw === '-' ? 0 : parseInt(additionsRaw, 10) || 0;
|
|
1145
|
+
const deletions = deletionsRaw === '-' ? 0 : parseInt(deletionsRaw, 10) || 0;
|
|
1146
|
+
totalAdditions += additions;
|
|
1147
|
+
totalDeletions += deletions;
|
|
1148
|
+
}
|
|
1149
|
+
logger.success('Commit diff fetched successfully', {
|
|
1150
|
+
repoName,
|
|
1151
|
+
commitHash: hash,
|
|
1152
|
+
filesChanged: statLines.length
|
|
1153
|
+
});
|
|
1154
|
+
return {
|
|
1155
|
+
repoName,
|
|
1156
|
+
commit: {
|
|
1157
|
+
hash: (hash || '').trim(),
|
|
1158
|
+
fullHash: (fullHash || '').trim(),
|
|
1159
|
+
author: (author || '').trim(),
|
|
1160
|
+
email: (email || '').trim(),
|
|
1161
|
+
date: (date || '').trim(),
|
|
1162
|
+
message: commitMessage
|
|
1163
|
+
},
|
|
1164
|
+
diff: diffResult.output || '',
|
|
1165
|
+
summary: {
|
|
1166
|
+
filesChanged: statLines.length,
|
|
1167
|
+
totalAdditions,
|
|
1168
|
+
totalDeletions,
|
|
1169
|
+
totalChanges: totalAdditions + totalDeletions
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
finally {
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Get commit details
|
|
1178
|
+
*/
|
|
1179
|
+
async getCommitDetails(repoName, commitHash) {
|
|
1180
|
+
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
1181
|
+
try {
|
|
1182
|
+
logger.info('Fetching commit details', { repoName, commitHash });
|
|
1183
|
+
// Get commit information and file changes in a single SSH call
|
|
1184
|
+
const combinedCommand = `
|
|
1185
|
+
cd ${repoPath} &&
|
|
1186
|
+
COMMIT_INFO=$(sudo git show --no-patch --pretty=format:"%H|%h|%an|%ae|%ad|%s" --date=iso ${commitHash}) &&
|
|
1187
|
+
FILE_STATS=$(sudo git show --stat --pretty="" --numstat ${commitHash}) &&
|
|
1188
|
+
echo "COMMIT_INFO:$COMMIT_INFO" &&
|
|
1189
|
+
echo "FILE_STATS_START" &&
|
|
1190
|
+
echo "$FILE_STATS" &&
|
|
1191
|
+
echo "FILE_STATS_END"
|
|
1192
|
+
`.replace(/\n\s+/g, ' ');
|
|
1193
|
+
const result = await executeShell(combinedCommand);
|
|
1194
|
+
if (!result.success) {
|
|
1195
|
+
throw new Error('Failed to fetch commit details');
|
|
1196
|
+
}
|
|
1197
|
+
// Parse commit info
|
|
1198
|
+
const output = result.output;
|
|
1199
|
+
const commitInfoMatch = output.match(/COMMIT_INFO:([^\n]+)/);
|
|
1200
|
+
if (!commitInfoMatch) {
|
|
1201
|
+
throw new Error('Failed to parse commit information');
|
|
1202
|
+
}
|
|
1203
|
+
const [fullHash, shortHash, author, email, date, message] = commitInfoMatch[1].split('|');
|
|
1204
|
+
// Parse file statistics
|
|
1205
|
+
const fileStatsMatch = output.match(/FILE_STATS_START\n([\s\S]*?)FILE_STATS_END/);
|
|
1206
|
+
const fileStatsRaw = fileStatsMatch ? fileStatsMatch[1].trim() : '';
|
|
1207
|
+
const modifiedFiles = [];
|
|
1208
|
+
let totalAdditions = 0;
|
|
1209
|
+
let totalDeletions = 0;
|
|
1210
|
+
if (fileStatsRaw) {
|
|
1211
|
+
const lines = fileStatsRaw.split('\n').filter((line) => line.trim());
|
|
1212
|
+
for (const line of lines) {
|
|
1213
|
+
// Format: additions\tdeletions\tfilename
|
|
1214
|
+
const parts = line.split('\t');
|
|
1215
|
+
if (parts.length >= 3) {
|
|
1216
|
+
const additions = parts[0] === '-' ? 0 : parseInt(parts[0]) || 0;
|
|
1217
|
+
const deletions = parts[1] === '-' ? 0 : parseInt(parts[1]) || 0;
|
|
1218
|
+
const filename = parts[2];
|
|
1219
|
+
// Determine file status
|
|
1220
|
+
let status = 'modified';
|
|
1221
|
+
if (additions > 0 && deletions === 0) {
|
|
1222
|
+
status = 'added';
|
|
1223
|
+
}
|
|
1224
|
+
else if (additions === 0 && deletions > 0) {
|
|
1225
|
+
status = 'deleted';
|
|
1226
|
+
}
|
|
1227
|
+
else if (parts[0] === '-' && parts[1] === '-') {
|
|
1228
|
+
status = 'binary';
|
|
1229
|
+
}
|
|
1230
|
+
modifiedFiles.push({
|
|
1231
|
+
filename,
|
|
1232
|
+
additions,
|
|
1233
|
+
deletions,
|
|
1234
|
+
changes: additions + deletions,
|
|
1235
|
+
status
|
|
1236
|
+
});
|
|
1237
|
+
totalAdditions += additions;
|
|
1238
|
+
totalDeletions += deletions;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
logger.success('Commit details fetched successfully', {
|
|
1243
|
+
repoName,
|
|
1244
|
+
commitHash: shortHash,
|
|
1245
|
+
filesModified: modifiedFiles.length
|
|
1246
|
+
});
|
|
1247
|
+
return {
|
|
1248
|
+
repoName,
|
|
1249
|
+
commit: {
|
|
1250
|
+
hash: shortHash,
|
|
1251
|
+
fullHash,
|
|
1252
|
+
author,
|
|
1253
|
+
email,
|
|
1254
|
+
date,
|
|
1255
|
+
message
|
|
1256
|
+
},
|
|
1257
|
+
files: modifiedFiles,
|
|
1258
|
+
summary: {
|
|
1259
|
+
totalFiles: modifiedFiles.length,
|
|
1260
|
+
totalAdditions,
|
|
1261
|
+
totalDeletions,
|
|
1262
|
+
totalChanges: totalAdditions + totalDeletions,
|
|
1263
|
+
filesByStatus: {
|
|
1264
|
+
added: modifiedFiles.filter(f => f.status === 'added').length,
|
|
1265
|
+
modified: modifiedFiles.filter(f => f.status === 'modified').length,
|
|
1266
|
+
deleted: modifiedFiles.filter(f => f.status === 'deleted').length,
|
|
1267
|
+
binary: modifiedFiles.filter(f => f.status === 'binary').length
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
finally {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
// Helper function to parse conflict markers in files
|
|
1276
|
+
async parseConflictMarkers(filePath, repoPath) {
|
|
1277
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1278
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1279
|
+
const fullPath = path.join(repoPath, filePath);
|
|
1280
|
+
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
|
1281
|
+
const lines = content.split('\n');
|
|
1282
|
+
const conflicts = [];
|
|
1283
|
+
let i = 0;
|
|
1284
|
+
while (i < lines.length) {
|
|
1285
|
+
if (lines[i].startsWith('<<<<<<<')) {
|
|
1286
|
+
const startLine = i;
|
|
1287
|
+
const currentBranch = lines[i].replace('<<<<<<< ', '').trim();
|
|
1288
|
+
// Find the separator
|
|
1289
|
+
let separatorLine = -1;
|
|
1290
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1291
|
+
if (lines[j].startsWith('=======')) {
|
|
1292
|
+
separatorLine = j;
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
// Find the end marker
|
|
1297
|
+
let endLine = -1;
|
|
1298
|
+
let incomingBranch = '';
|
|
1299
|
+
for (let j = separatorLine + 1; j < lines.length; j++) {
|
|
1300
|
+
if (lines[j].startsWith('>>>>>>>')) {
|
|
1301
|
+
endLine = j;
|
|
1302
|
+
incomingBranch = lines[j].replace('>>>>>>> ', '').trim();
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (separatorLine !== -1 && endLine !== -1) {
|
|
1307
|
+
// Check if there's a base content (3-way merge marker |||||||)
|
|
1308
|
+
let baseContent;
|
|
1309
|
+
let baseMarkerLine = -1;
|
|
1310
|
+
for (let j = i + 1; j < separatorLine; j++) {
|
|
1311
|
+
if (lines[j].startsWith('|||||||')) {
|
|
1312
|
+
baseMarkerLine = j;
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (baseMarkerLine !== -1) {
|
|
1317
|
+
// 3-way merge
|
|
1318
|
+
const actualCurrentContent = lines.slice(i + 1, baseMarkerLine).join('\n');
|
|
1319
|
+
baseContent = lines.slice(baseMarkerLine + 1, separatorLine).join('\n');
|
|
1320
|
+
const incomingContent = lines.slice(separatorLine + 1, endLine).join('\n');
|
|
1321
|
+
conflicts.push({
|
|
1322
|
+
startLine: startLine + 1, // 1-based line numbers
|
|
1323
|
+
endLine: endLine + 1,
|
|
1324
|
+
currentContent: actualCurrentContent,
|
|
1325
|
+
incomingContent: incomingContent,
|
|
1326
|
+
baseContent: baseContent,
|
|
1327
|
+
currentBranch: currentBranch,
|
|
1328
|
+
incomingBranch: incomingBranch
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
// 2-way merge
|
|
1333
|
+
const currentContent = lines.slice(i + 1, separatorLine).join('\n');
|
|
1334
|
+
const incomingContent = lines.slice(separatorLine + 1, endLine).join('\n');
|
|
1335
|
+
conflicts.push({
|
|
1336
|
+
startLine: startLine + 1,
|
|
1337
|
+
endLine: endLine + 1,
|
|
1338
|
+
currentContent: currentContent,
|
|
1339
|
+
incomingContent: incomingContent,
|
|
1340
|
+
currentBranch: currentBranch,
|
|
1341
|
+
incomingBranch: incomingBranch
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
i = endLine + 1;
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
i++;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
i++;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return conflicts;
|
|
1355
|
+
}
|
|
1356
|
+
async getMergeConflictsImplementation(repoName, folderPath) {
|
|
1357
|
+
console.log('[Merge Conflicts] ===============================');
|
|
1358
|
+
console.log('[Merge Conflicts] Checking for conflicts in repo:', repoName);
|
|
1359
|
+
try {
|
|
1360
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1361
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1362
|
+
// Resolve full repo path from base folderPath and repoName
|
|
1363
|
+
const repoPath = path.join(folderPath, repoName);
|
|
1364
|
+
console.log(`[Merge Conflicts] Resolved repo path: ${repoPath}`);
|
|
1365
|
+
// Check if the repo path exists
|
|
1366
|
+
if (!fs.existsSync(repoPath)) {
|
|
1367
|
+
return {
|
|
1368
|
+
success: false,
|
|
1369
|
+
error: `Repository path does not exist: ${repoPath}`,
|
|
1370
|
+
folderPath: folderPath,
|
|
1371
|
+
repoName: repoName
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
// Check if it's a git repository
|
|
1375
|
+
const gitPath = path.join(repoPath, '.git');
|
|
1376
|
+
if (!fs.existsSync(gitPath)) {
|
|
1377
|
+
return {
|
|
1378
|
+
success: false,
|
|
1379
|
+
error: `Not a git repository: ${repoPath}`,
|
|
1380
|
+
folderPath: folderPath,
|
|
1381
|
+
repoName: repoName
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
const git = (0, simple_git_1.simpleGit)(repoPath);
|
|
1385
|
+
const status = await git.status();
|
|
1386
|
+
const currentBranch = status.current || 'unknown';
|
|
1387
|
+
console.log(`[Merge Conflicts] Current branch: ${currentBranch}`);
|
|
1388
|
+
console.log(`[Merge Conflicts] Conflicted files: ${status.conflicted.length}`);
|
|
1389
|
+
console.log(`[Merge Conflicts] Is merging: ${status.conflicted.length > 0 ? 'Yes' : 'No'}`);
|
|
1390
|
+
if (status.conflicted.length === 0) {
|
|
1391
|
+
console.log(`[Merge Conflicts] No conflicts found in repository`);
|
|
1392
|
+
return {
|
|
1393
|
+
success: true,
|
|
1394
|
+
hasConflicts: false,
|
|
1395
|
+
conflicts: [],
|
|
1396
|
+
message: 'No merge conflicts detected in the repository.',
|
|
1397
|
+
currentBranch: currentBranch,
|
|
1398
|
+
folderPath: folderPath,
|
|
1399
|
+
repoName: repoName
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
const conflictFiles = [];
|
|
1403
|
+
let totalConflictsCount = 0;
|
|
1404
|
+
console.log(`[Merge Conflicts] Processing ${status.conflicted.length} conflicted file(s)...`);
|
|
1405
|
+
for (const conflictedFile of status.conflicted) {
|
|
1406
|
+
try {
|
|
1407
|
+
const fullPath = path.join(repoPath, conflictedFile);
|
|
1408
|
+
// Check if file exists
|
|
1409
|
+
if (!fs.existsSync(fullPath)) {
|
|
1410
|
+
console.warn(`[Merge Conflicts] File not found: ${conflictedFile}`);
|
|
1411
|
+
conflictFiles.push({
|
|
1412
|
+
file: conflictedFile,
|
|
1413
|
+
conflicts: [],
|
|
1414
|
+
fullContent: ''
|
|
1415
|
+
});
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const fullContent = await fs.promises.readFile(fullPath, 'utf-8');
|
|
1419
|
+
// Parse conflict markers
|
|
1420
|
+
const conflicts = await this.parseConflictMarkers(conflictedFile, repoPath);
|
|
1421
|
+
totalConflictsCount += conflicts.length;
|
|
1422
|
+
conflictFiles.push({
|
|
1423
|
+
file: conflictedFile,
|
|
1424
|
+
conflicts: conflicts,
|
|
1425
|
+
fullContent: fullContent
|
|
1426
|
+
});
|
|
1427
|
+
console.log(`[Merge Conflicts] File: ${conflictedFile} - ${conflicts.length} conflict block(s) found`);
|
|
1428
|
+
conflicts.forEach((conflict, index) => {
|
|
1429
|
+
console.log(`[Merge Conflicts] Conflict #${index + 1}: Lines ${conflict.startLine}-${conflict.endLine}`);
|
|
1430
|
+
console.log(`[Merge Conflicts] Current (${conflict.currentBranch}): ${conflict.currentContent.substring(0, 50)}...`);
|
|
1431
|
+
console.log(`[Merge Conflicts] Incoming (${conflict.incomingBranch}): ${conflict.incomingContent.substring(0, 50)}...`);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
catch (parseError) {
|
|
1435
|
+
console.error(`[Merge Conflicts] Error parsing ${conflictedFile}:`, parseError);
|
|
1436
|
+
conflictFiles.push({
|
|
1437
|
+
file: conflictedFile,
|
|
1438
|
+
conflicts: [],
|
|
1439
|
+
fullContent: ''
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
console.log(`[Merge Conflicts] Summary: ${conflictFiles.length} file(s) with ${totalConflictsCount} total conflict block(s)`);
|
|
1444
|
+
// Send response with all conflict information
|
|
1445
|
+
return {
|
|
1446
|
+
success: true,
|
|
1447
|
+
hasConflicts: true,
|
|
1448
|
+
conflicts: conflictFiles,
|
|
1449
|
+
message: `Found ${totalConflictsCount} conflict block(s) in ${conflictFiles.length} file(s).`,
|
|
1450
|
+
currentBranch: currentBranch,
|
|
1451
|
+
totalFiles: conflictFiles.length,
|
|
1452
|
+
totalConflicts: totalConflictsCount,
|
|
1453
|
+
folderPath: folderPath,
|
|
1454
|
+
repoName: repoName
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
catch (error) {
|
|
1458
|
+
console.error('[Merge Conflicts] Error checking for conflicts:', error);
|
|
1459
|
+
return {
|
|
1460
|
+
success: false,
|
|
1461
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred while checking for merge conflicts',
|
|
1462
|
+
folderPath: folderPath,
|
|
1463
|
+
repoName: repoName
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Helper method to get merge conflicts
|
|
1469
|
+
*/
|
|
1470
|
+
async getMergeConflicts(socket, repoName) {
|
|
1471
|
+
return new Promise((resolve) => {
|
|
1472
|
+
this.getMergeConflictsImplementation(repoName, tool_server_1.folderPath).then((mergeData) => {
|
|
1473
|
+
resolve(mergeData);
|
|
1474
|
+
}).catch((error) => {
|
|
1475
|
+
console.error('Error in getMergeConflictsImplementation:', error);
|
|
1476
|
+
resolve({
|
|
1477
|
+
success: false,
|
|
1478
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred while getting merge conflicts',
|
|
1479
|
+
folderPath: tool_server_1.folderPath,
|
|
1480
|
+
repoName: repoName
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
exports.GithubOperationsService = GithubOperationsService;
|
|
1487
|
+
//# sourceMappingURL=githubOperationsHanlder.js.map
|