buddy-workbench 0.1.73 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.projectKey === parsed.projectKey && identity.repositorySlug === parsed.repositorySlug) return folder;
90
+ if (identity.repositorySlug && parsed.repositorySlug && identity.repositorySlug.toLowerCase() === parsed.repositorySlug.toLowerCase()) return folder;
91
91
  }
92
- throw new Error(`Could not find ${parsed.projectKey}/${parsed.repositorySlug} in workspace: ${workspaceDir}`);
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
- await git(folder, ['checkout', '-b', newBranchName]);
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
- const content = await readGitFile(folder, choiceRef, filePath);
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
- await git(folder, ['add', '--', ...resolvedPaths]);
264
- const staged = String(await git(folder, ['diff', '--cached', '--name-only'])).trim();
265
- if (!staged) return res.status(409).json({ error: 'No file changes to commit. The selected versions already match the source branch.' });
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 contains the selected resolutions for Pull Request #${parsed.pullRequestId}.`,
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
- res.status(500).json({ error: `Unable to create resolution commit: ${error.stderr || error.message}` });
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 {}