buddy-workbench 0.1.72 → 0.1.74
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/package.json +4 -1
- package/server/routes/overview.js +13 -88
- package/server/routes/pr-conflicts.js +110 -17
- package/ui/dist/assets/{index-D7Y6Ks9X.js → index-BqVFfTXR.js} +95 -95
- package/ui/dist/assets/index-CAdLVTJk.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-CZUbxhFl.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.74",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -59,5 +59,8 @@
|
|
|
59
59
|
"@jsquash/jpeg": "1.6.0",
|
|
60
60
|
"axios": "1.7.9",
|
|
61
61
|
"express": "5.1.0"
|
|
62
|
+
},
|
|
63
|
+
"overrides": {
|
|
64
|
+
"content-type": "2.0.0"
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -3,8 +3,6 @@ import axios from 'axios';
|
|
|
3
3
|
import { Router } from 'express';
|
|
4
4
|
import { readSettings } from '../repositories/settings.js';
|
|
5
5
|
import { listTodos } from '../repositories/todos.js';
|
|
6
|
-
import { listBranchSyncApps } from '../repositories/branch-sync.js';
|
|
7
|
-
import { getPresentations } from '../repositories/presentations.js';
|
|
8
6
|
import { recordApiError } from '../lib/api-errors.js';
|
|
9
7
|
|
|
10
8
|
const router = Router();
|
|
@@ -16,16 +14,6 @@ const httpClient = axios.create({
|
|
|
16
14
|
|
|
17
15
|
const isSuccess = (response) => response.status >= 200 && response.status < 300;
|
|
18
16
|
const apiMessage = (response, fallback) => response?.data?.errors?.[0]?.message || response?.data?.errorMessages?.[0] || response?.data?.message || response?.statusText || fallback;
|
|
19
|
-
const getPresentationInsights = () => getPresentations().plans
|
|
20
|
-
.slice()
|
|
21
|
-
.sort((a, b) => Number(b.updatedAt || b.createdAt || 0) - Number(a.updatedAt || a.createdAt || 0))
|
|
22
|
-
.map((plan) => ({
|
|
23
|
-
id: plan.id,
|
|
24
|
-
title: plan.title || plan.name || 'Untitled presentation',
|
|
25
|
-
summary: `${Array.isArray(plan.sections) ? plan.sections.length : 0} sections`,
|
|
26
|
-
updated: plan.updatedAt || plan.createdAt || null,
|
|
27
|
-
url: '#/presentation-plan'
|
|
28
|
-
}));
|
|
29
17
|
|
|
30
18
|
function jiraHost(domain) {
|
|
31
19
|
const clean = String(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
|
@@ -39,20 +27,7 @@ function bitbucketHost(domain) {
|
|
|
39
27
|
return clean.includes('bitbucket') ? clean : `bitbucket.${clean}`;
|
|
40
28
|
}
|
|
41
29
|
|
|
42
|
-
function parseRepo(repo) {
|
|
43
|
-
const raw = String(repo || '').replace(/\.git$/i, '');
|
|
44
|
-
try {
|
|
45
|
-
const url = new URL(raw.startsWith('ssh://') ? raw.replace(/^ssh:\/\//i, 'https://') : raw);
|
|
46
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
47
|
-
if (parts[0] === 'scm' && parts.length >= 3) return { host: url.host, project: parts[1], slug: parts[2] };
|
|
48
|
-
if ((parts[0] === 'projects' || parts[0] === 'users') && parts.length >= 4) return { host: url.host, project: parts[0] === 'users' ? `~${parts[1]}` : parts[1], slug: parts[3] };
|
|
49
|
-
if (parts.length >= 2) return { host: url.host, project: parts.at(-2), slug: parts.at(-1) };
|
|
50
|
-
} catch {}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
30
|
function jiraUrl(host, path) { return `https://${host}/rest/api/2${path}`; }
|
|
55
|
-
function bitbucketUrl(host, path) { return `https://${host}/rest/api/latest${path}`; }
|
|
56
31
|
|
|
57
32
|
async function requestSection(name, loader) {
|
|
58
33
|
try {
|
|
@@ -95,10 +70,9 @@ async function loadJiraData(host, token) {
|
|
|
95
70
|
const identity = { accountId: me.accountId || '', name: me.name || '', displayName: me.displayName || '' };
|
|
96
71
|
const fields = 'summary,status,priority,duedate,updated,assignee,reporter';
|
|
97
72
|
|
|
98
|
-
const [due, activity
|
|
73
|
+
const [due, activity] = await Promise.all([
|
|
99
74
|
searchJira(host, token, 'assignee = currentUser() AND due = startOfDay() AND resolution = Unresolved ORDER BY priority DESC', fields),
|
|
100
|
-
searchJira(host, token, 'updated >= -14d AND resolution = Unresolved AND (assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC', fields, 30)
|
|
101
|
-
searchJira(host, token, 'status = Blocked AND updated <= -3d AND resolution = Unresolved ORDER BY updated ASC', fields)
|
|
75
|
+
searchJira(host, token, 'updated >= -14d AND resolution = Unresolved AND (assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC', fields, 30)
|
|
102
76
|
]);
|
|
103
77
|
|
|
104
78
|
const activityItems = [];
|
|
@@ -130,15 +104,14 @@ async function loadJiraData(host, token) {
|
|
|
130
104
|
return {
|
|
131
105
|
identity,
|
|
132
106
|
dueToday: due.map((issue) => ({ ...mapJiraIssue(issue), url: issueUrl(host, issue.key) })),
|
|
133
|
-
activity: activityItems.map((item) => ({ ...item, url: issueUrl(host, item.key) }))
|
|
134
|
-
blocked: blocked.map((issue) => ({ ...mapJiraIssue(issue), url: issueUrl(host, issue.key) }))
|
|
107
|
+
activity: activityItems.map((item) => ({ ...item, url: issueUrl(host, item.key) }))
|
|
135
108
|
};
|
|
136
109
|
}
|
|
137
110
|
|
|
138
|
-
async function
|
|
139
|
-
const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role
|
|
111
|
+
async function loadAuthoredPullRequests(host, token) {
|
|
112
|
+
const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
|
|
140
113
|
const response = await httpClient.get(url, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } });
|
|
141
|
-
if (!isSuccess(response)) throw new Error(`Bitbucket
|
|
114
|
+
if (!isSuccess(response)) throw new Error(`Bitbucket author pull requests failed (${response.status}): ${apiMessage(response, 'Request failed')}`);
|
|
142
115
|
return (response.data?.values || []).map((pr) => ({
|
|
143
116
|
id: pr.id,
|
|
144
117
|
title: pr.title || `Pull request #${pr.id}`,
|
|
@@ -149,65 +122,21 @@ async function loadPullRequests(host, token, role) {
|
|
|
149
122
|
}));
|
|
150
123
|
}
|
|
151
124
|
|
|
152
|
-
async function loadFailedBuilds(host, token) {
|
|
153
|
-
const settings = readSettings();
|
|
154
|
-
const apps = listBranchSyncApps();
|
|
155
|
-
const repos = [];
|
|
156
|
-
for (const app of apps) {
|
|
157
|
-
for (const branch of [...new Set([app.activeBranch, app.lastReleaseBranch, 'master'].filter(Boolean))]) {
|
|
158
|
-
repos.push({ name: app.name, repo: app.repo, branch });
|
|
159
|
-
}
|
|
160
|
-
for (const sub of app.subApps || []) {
|
|
161
|
-
for (const branch of [...new Set([sub.branch, app.activeBranch, 'master'].filter(Boolean))]) {
|
|
162
|
-
repos.push({ name: sub.name || sub.directory, repo: sub.repo, branch });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const unique = [...new Map(repos.map((item) => [`${item.repo}|${item.branch}`, item])).values()];
|
|
167
|
-
const failed = [];
|
|
168
|
-
for (const item of unique.slice(0, 30)) {
|
|
169
|
-
const parsed = parseRepo(item.repo);
|
|
170
|
-
if (!parsed || (parsed.host && parsed.host !== host && !parsed.host.includes('bitbucket'))) continue;
|
|
171
|
-
const commitUrl = bitbucketUrl(parsed.host || host, `/projects/${encodeURIComponent(parsed.project)}/repos/${encodeURIComponent(parsed.slug)}/commits?until=${encodeURIComponent(item.branch)}&limit=1`);
|
|
172
|
-
const commitResponse = await httpClient.get(commitUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } });
|
|
173
|
-
if (!isSuccess(commitResponse)) {
|
|
174
|
-
recordApiError({ source: 'Overview (Bitbucket Builds)', method: 'GET', url: commitUrl, status: commitResponse.status, message: apiMessage(commitResponse, 'Failed to fetch branch commit.') });
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const hash = commitResponse.data?.values?.[0]?.id;
|
|
178
|
-
if (!hash) continue;
|
|
179
|
-
const buildUrl = `https://${parsed.host || host}/rest/api/latest/projects/${encodeURIComponent(parsed.project)}/repos/${encodeURIComponent(parsed.slug)}/commits/${encodeURIComponent(hash)}/builds`;
|
|
180
|
-
const buildResponse = await httpClient.get(buildUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } });
|
|
181
|
-
if (!isSuccess(buildResponse)) {
|
|
182
|
-
if (buildResponse.status !== 404) recordApiError({ source: 'Overview (Bitbucket Builds)', method: 'GET', url: buildUrl, status: buildResponse.status, message: apiMessage(buildResponse, 'Failed to fetch build status.') });
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
for (const build of buildResponse.data?.values || []) {
|
|
186
|
-
if (String(build.state || '').toUpperCase() === 'FAILED') {
|
|
187
|
-
failed.push({ name: item.name, branch: item.branch, repository: item.repo, build: build.name || build.key || 'Build', date: build.dateAdded || null, url: item.repo });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return failed;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
125
|
router.get('/', async (_req, res) => {
|
|
195
126
|
const settings = readSettings();
|
|
196
|
-
const missingTokens = ['jira', 'bitbucket'
|
|
127
|
+
const missingTokens = ['jira', 'bitbucket'].filter((service) => !settings[`${service}AccessToken`]);
|
|
197
128
|
if (missingTokens.length > 0) {
|
|
198
|
-
return res.json({ configured: false, missingTokens, fetchedAt: new Date().toISOString(), categories: {
|
|
129
|
+
return res.json({ configured: false, missingTokens, fetchedAt: new Date().toISOString(), categories: {} });
|
|
199
130
|
}
|
|
200
131
|
const jira = jiraHost(settings.domain);
|
|
201
132
|
const bitbucket = bitbucketHost(settings.domain);
|
|
202
|
-
if (!jira || !bitbucket) return res.json({ configured: false, missingTokens: ['domain'], fetchedAt: new Date().toISOString(), categories: {
|
|
133
|
+
if (!jira || !bitbucket) return res.json({ configured: false, missingTokens: ['domain'], fetchedAt: new Date().toISOString(), categories: {} });
|
|
203
134
|
|
|
204
135
|
const sections = await Promise.all([
|
|
205
136
|
requestSection('Jira', () => loadJiraData(jira, settings.jiraAccessToken)),
|
|
206
|
-
requestSection('Bitbucket
|
|
207
|
-
requestSection('Bitbucket Author', () => loadPullRequests(bitbucket, settings.bitbucketAccessToken, 'author')),
|
|
208
|
-
requestSection('Bitbucket Builds', () => loadFailedBuilds(bitbucket, settings.bitbucketAccessToken))
|
|
137
|
+
requestSection('Bitbucket Author', () => loadAuthoredPullRequests(bitbucket, settings.bitbucketAccessToken))
|
|
209
138
|
]);
|
|
210
|
-
const [jiraData,
|
|
139
|
+
const [jiraData, authoredPrs] = sections;
|
|
211
140
|
const todos = listTodos().filter((todo) => !todo.completed && !todo.archived).map((todo) => ({ ...todo, url: '#/todo-list' }));
|
|
212
141
|
const errors = sections.filter((section) => section.error).map((section) => section.error);
|
|
213
142
|
res.json({
|
|
@@ -215,14 +144,10 @@ router.get('/', async (_req, res) => {
|
|
|
215
144
|
missingTokens: [],
|
|
216
145
|
fetchedAt: new Date().toISOString(),
|
|
217
146
|
categories: {
|
|
147
|
+
activity: jiraData.items?.activity || [],
|
|
218
148
|
jiraDueToday: jiraData.items?.dueToday || [],
|
|
219
149
|
assignedTodos: todos,
|
|
220
|
-
|
|
221
|
-
authoredPrs: authoredPrs.items || [],
|
|
222
|
-
failedBuilds: failedBuilds.items || [],
|
|
223
|
-
activity: jiraData.items?.activity || [],
|
|
224
|
-
blockedIssues: jiraData.items?.blocked || [],
|
|
225
|
-
presentations: getPresentationInsights()
|
|
150
|
+
authoredPrs: authoredPrs.items || []
|
|
226
151
|
},
|
|
227
152
|
errors
|
|
228
153
|
});
|
|
@@ -87,9 +87,9 @@ async function findWorkspaceRepository(parsed) {
|
|
|
87
87
|
|
|
88
88
|
for (const folder of candidates) {
|
|
89
89
|
const identity = remoteIdentity(await getGitRemoteUrl(folder));
|
|
90
|
-
if (identity.
|
|
90
|
+
if (identity.repositorySlug && parsed.repositorySlug && identity.repositorySlug.toLowerCase() === parsed.repositorySlug.toLowerCase()) return folder;
|
|
91
91
|
}
|
|
92
|
-
throw new Error(`Could not find ${parsed.
|
|
92
|
+
throw new Error(`Could not find ${parsed.repositorySlug} in workspace: ${workspaceDir}`);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function validateRelativeFilePath(filePath) {
|
|
@@ -188,6 +188,74 @@ router.get('/my-conflicts', async (_req, res) => {
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
router.post('/check-branch', async (req, res) => {
|
|
192
|
+
const parsed = parsePrUrl(req.body?.prUrl);
|
|
193
|
+
const branchName = typeof req.body?.branchName === 'string' ? req.body.branchName.trim() : '';
|
|
194
|
+
if (!branchName) return res.json({ exists: false });
|
|
195
|
+
if (!parsed) return res.status(400).json({ error: 'Invalid Pull Request URL.' });
|
|
196
|
+
|
|
197
|
+
// 1. Validate ref format
|
|
198
|
+
if (/[\s~^:?*\[\\]|\.\.|\/\.|\/\/|@\{|^\/|\/$|\.lock$/i.test(branchName)) {
|
|
199
|
+
return res.json({ exists: true, error: `Invalid branch name format: ${branchName}` });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 2. Try checking local repo if workspace exists
|
|
203
|
+
try {
|
|
204
|
+
const folder = await findWorkspaceRepository(parsed);
|
|
205
|
+
try {
|
|
206
|
+
await git(folder, ['check-ref-format', '--branch', branchName]);
|
|
207
|
+
} catch {
|
|
208
|
+
return res.json({ exists: true, error: `Invalid branch name: ${branchName}` });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await git(folder, ['rev-parse', '--verify', `refs/heads/${branchName}`]);
|
|
213
|
+
return res.json({ exists: true, error: `Local branch already exists: ${branchName}` });
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await git(folder, ['ls-remote', '--exit-code', '--heads', 'origin', branchName]);
|
|
218
|
+
return res.json({ exists: true, error: `Remote branch already exists on origin: ${branchName}` });
|
|
219
|
+
} catch {}
|
|
220
|
+
} catch {
|
|
221
|
+
// If local repo cannot be located, fallback to Bitbucket REST API
|
|
222
|
+
try {
|
|
223
|
+
const branchesUrl = `https://${parsed.host}/rest/api/latest/projects/${encodeURIComponent(parsed.projectKey)}/repos/${encodeURIComponent(parsed.repositorySlug)}/branches?filterText=${encodeURIComponent(branchName)}&limit=10`;
|
|
224
|
+
const response = await client.get(branchesUrl, { headers: authHeaders() });
|
|
225
|
+
if (response.status >= 200 && response.status < 300) {
|
|
226
|
+
const values = Array.isArray(response.data?.values) ? response.data.values : [];
|
|
227
|
+
const found = values.find((b) => (b.displayId || b.id?.replace('refs/heads/', '')) === branchName);
|
|
228
|
+
if (found) {
|
|
229
|
+
return res.json({ exists: true, error: `Remote branch already exists on origin: ${branchName}` });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return res.json({ exists: false });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
async function applyFileChoice(folder, filePath, choiceRef) {
|
|
239
|
+
try {
|
|
240
|
+
await git(folder, ['checkout', choiceRef, '--', filePath]);
|
|
241
|
+
await git(folder, ['add', '--', filePath]);
|
|
242
|
+
} catch {
|
|
243
|
+
const content = await readGitFile(folder, choiceRef, filePath);
|
|
244
|
+
const absolutePath = join(folder, filePath);
|
|
245
|
+
if (content === null) {
|
|
246
|
+
if (existsSync(absolutePath)) rmSync(absolutePath, { force: true, recursive: true });
|
|
247
|
+
try {
|
|
248
|
+
await git(folder, ['rm', '-f', '--', filePath]);
|
|
249
|
+
} catch {
|
|
250
|
+
await git(folder, ['add', '-A', '--', filePath]);
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
writeFileSync(absolutePath, content);
|
|
254
|
+
await git(folder, ['add', '--', filePath]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
191
259
|
router.post('/resolve', async (req, res) => {
|
|
192
260
|
const parsed = parsePrUrl(req.body?.prUrl);
|
|
193
261
|
const files = Array.isArray(req.body?.files) ? req.body.files : [];
|
|
@@ -228,7 +296,6 @@ router.post('/resolve', async (req, res) => {
|
|
|
228
296
|
const targetRef = `origin/${targetBranch}`;
|
|
229
297
|
await git(folder, ['rev-parse', '--verify', sourceRef]);
|
|
230
298
|
await git(folder, ['rev-parse', '--verify', targetRef]);
|
|
231
|
-
await git(folder, ['checkout', '-B', sourceBranch, sourceRef]);
|
|
232
299
|
|
|
233
300
|
let commitBranch = sourceBranch;
|
|
234
301
|
if (createNewBranch) {
|
|
@@ -241,28 +308,48 @@ router.post('/resolve', async (req, res) => {
|
|
|
241
308
|
await git(folder, ['ls-remote', '--exit-code', '--heads', 'origin', newBranchName]);
|
|
242
309
|
return res.status(409).json({ error: `Remote branch already exists: ${newBranchName}` });
|
|
243
310
|
} catch {}
|
|
244
|
-
|
|
311
|
+
|
|
312
|
+
// Checkout from target branch (b), then create resolution branch
|
|
313
|
+
await git(folder, ['checkout', '-B', newBranchName, targetRef]);
|
|
245
314
|
commitBranch = newBranchName;
|
|
315
|
+
|
|
316
|
+
// Merge source branch (a) into resolution branch
|
|
317
|
+
try {
|
|
318
|
+
await git(folder, ['merge', '--no-commit', '--no-ff', sourceRef]);
|
|
319
|
+
} catch {
|
|
320
|
+
// Merge conflicts are expected and will be resolved per file choices
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Direct resolution on source branch (a) by merging target branch (b)
|
|
324
|
+
await git(folder, ['checkout', '-B', sourceBranch, sourceRef]);
|
|
325
|
+
commitBranch = sourceBranch;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await git(folder, ['merge', '--no-commit', '--no-ff', targetRef]);
|
|
329
|
+
} catch {
|
|
330
|
+
// Merge conflicts are expected and will be resolved per file choices
|
|
331
|
+
}
|
|
246
332
|
}
|
|
247
333
|
|
|
248
334
|
const resolvedPaths = [];
|
|
249
335
|
for (const file of files) {
|
|
250
336
|
const filePath = validateRelativeFilePath(file.path);
|
|
251
337
|
const choiceRef = choices[file.path] === 'target' ? targetRef : sourceRef;
|
|
252
|
-
|
|
253
|
-
const absolutePath = join(folder, filePath);
|
|
254
|
-
if (content === null) {
|
|
255
|
-
if (choices[file.path] === 'target' && existsSync(absolutePath)) rmSync(absolutePath);
|
|
256
|
-
else if (choices[file.path] === 'source') throw new Error(`File not found on source branch: ${filePath}`);
|
|
257
|
-
} else {
|
|
258
|
-
writeFileSync(absolutePath, content);
|
|
259
|
-
}
|
|
338
|
+
await applyFileChoice(folder, filePath, choiceRef);
|
|
260
339
|
resolvedPaths.push(filePath);
|
|
261
340
|
}
|
|
262
341
|
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
342
|
+
// Resolve any remaining unmerged paths that might not have been in the files array
|
|
343
|
+
const unmergedStr = String(await git(folder, ['diff', '--name-only', '--diff-filter=U'])).trim();
|
|
344
|
+
if (unmergedStr) {
|
|
345
|
+
const unmergedFiles = unmergedStr.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
346
|
+
for (const unmergedFile of unmergedFiles) {
|
|
347
|
+
const choiceRef = choices[unmergedFile] === 'target' ? targetRef : sourceRef;
|
|
348
|
+
await applyFileChoice(folder, unmergedFile, choiceRef);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Commit merge resolution
|
|
266
353
|
await git(folder, ['commit', '-m', commitMessage]);
|
|
267
354
|
await git(folder, ['push', 'origin', commitBranch]);
|
|
268
355
|
const commitId = String(await git(folder, ['rev-parse', 'HEAD'])).trim();
|
|
@@ -272,7 +359,7 @@ router.post('/resolve', async (req, res) => {
|
|
|
272
359
|
const createPrUrl = `https://${parsed.host}/rest/api/latest/projects/${encodeURIComponent(parsed.projectKey)}/repos/${encodeURIComponent(parsed.repositorySlug)}/pull-requests`;
|
|
273
360
|
const createPrResponse = await client.post(createPrUrl, {
|
|
274
361
|
title: commitMessage,
|
|
275
|
-
description: `Resolution Pull Request created from ${commitBranch}.\n\nThis branch
|
|
362
|
+
description: `Resolution Pull Request created from ${commitBranch}.\n\nThis branch resolves conflicts for Pull Request #${parsed.pullRequestId} (${sourceBranch} → ${targetBranch}).`,
|
|
276
363
|
fromRef: {
|
|
277
364
|
id: `refs/heads/${commitBranch}`,
|
|
278
365
|
repository: { slug: parsed.repositorySlug, project: { key: parsed.projectKey } }
|
|
@@ -300,10 +387,16 @@ router.post('/resolve', async (req, res) => {
|
|
|
300
387
|
prUrl: resolutionPrUrl
|
|
301
388
|
});
|
|
302
389
|
} catch (error) {
|
|
303
|
-
|
|
390
|
+
const errorMsg = (error.stderr ? String(error.stderr).trim() : '') || error.message || String(error);
|
|
391
|
+
res.status(500).json({ error: `Unable to create resolution commit: ${errorMsg}` });
|
|
304
392
|
} finally {
|
|
305
393
|
if (folder) {
|
|
306
394
|
try {
|
|
395
|
+
if (existsSync(join(folder, '.git', 'MERGE_HEAD'))) {
|
|
396
|
+
await git(folder, ['merge', '--abort']).catch(() => {});
|
|
397
|
+
}
|
|
398
|
+
await git(folder, ['reset', '--hard', 'HEAD']).catch(() => {});
|
|
399
|
+
await git(folder, ['clean', '-fd']).catch(() => {});
|
|
307
400
|
if (originalBranch) await git(folder, ['checkout', originalBranch]);
|
|
308
401
|
else if (originalHead) await git(folder, ['checkout', '--detach', originalHead]);
|
|
309
402
|
} catch {}
|