azdo-cli 0.5.0-017-pr-comments-threads.242 → 0.5.0-017-pr-comments-threads.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +215 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2396,6 +2396,14 @@ function mapThread(thread) {
|
|
|
2396
2396
|
comments
|
|
2397
2397
|
};
|
|
2398
2398
|
}
|
|
2399
|
+
function toActiveCommentThread(thread) {
|
|
2400
|
+
return {
|
|
2401
|
+
id: thread.id,
|
|
2402
|
+
status: thread.status,
|
|
2403
|
+
threadContext: thread.threadContext?.filePath ?? null,
|
|
2404
|
+
comments: thread.comments.map(mapComment).filter((comment) => comment !== null)
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2399
2407
|
var RESOLVED_THREAD_STATUSES = /* @__PURE__ */ new Set(["fixed", "wontFix", "closed", "byDesign"]);
|
|
2400
2408
|
function isThreadResolved(status) {
|
|
2401
2409
|
return RESOLVED_THREAD_STATUSES.has(status);
|
|
@@ -2406,6 +2414,31 @@ async function readJsonResponse(response) {
|
|
|
2406
2414
|
}
|
|
2407
2415
|
return response.json();
|
|
2408
2416
|
}
|
|
2417
|
+
async function patchThreadStatus(context, repo, pat, prId, threadId, status) {
|
|
2418
|
+
const url = new URL(
|
|
2419
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}`
|
|
2420
|
+
);
|
|
2421
|
+
url.searchParams.set("api-version", "7.1");
|
|
2422
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
2423
|
+
method: "PATCH",
|
|
2424
|
+
headers: {
|
|
2425
|
+
...authHeaders(pat),
|
|
2426
|
+
"Content-Type": "application/json"
|
|
2427
|
+
},
|
|
2428
|
+
body: JSON.stringify({ status })
|
|
2429
|
+
});
|
|
2430
|
+
const data = await readJsonResponse(response);
|
|
2431
|
+
return toActiveCommentThread(data);
|
|
2432
|
+
}
|
|
2433
|
+
async function getPullRequestById(context, repo, pat, prId) {
|
|
2434
|
+
const url = new URL(
|
|
2435
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}`
|
|
2436
|
+
);
|
|
2437
|
+
url.searchParams.set("api-version", "7.1");
|
|
2438
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
2439
|
+
const data = await readJsonResponse(response);
|
|
2440
|
+
return mapPullRequest(repo, data);
|
|
2441
|
+
}
|
|
2409
2442
|
async function listPullRequests(context, repo, pat, sourceBranch, opts) {
|
|
2410
2443
|
const response = await fetchWithErrors(
|
|
2411
2444
|
buildPullRequestsUrl(context, repo, sourceBranch, opts).toString(),
|
|
@@ -2475,6 +2508,13 @@ async function getPullRequestThreads(context, repo, pat, prId) {
|
|
|
2475
2508
|
}
|
|
2476
2509
|
|
|
2477
2510
|
// src/commands/pr.ts
|
|
2511
|
+
function parsePositivePrNumber(raw) {
|
|
2512
|
+
if (!/^\d+$/.test(raw)) {
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
const n = Number.parseInt(raw, 10);
|
|
2516
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
2517
|
+
}
|
|
2478
2518
|
function formatBranchName(refName) {
|
|
2479
2519
|
return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
|
|
2480
2520
|
}
|
|
@@ -2542,10 +2582,20 @@ function formatThreads(prId, title, threads) {
|
|
|
2542
2582
|
}
|
|
2543
2583
|
return lines.join("\n");
|
|
2544
2584
|
}
|
|
2545
|
-
async function resolvePrCommandContext(options) {
|
|
2585
|
+
async function resolvePrCommandContext(options, resolveOpts = {}) {
|
|
2586
|
+
const requireBranch = resolveOpts.requireBranch ?? true;
|
|
2546
2587
|
const context = resolveContext(options);
|
|
2547
2588
|
const repo = detectRepoName();
|
|
2548
|
-
|
|
2589
|
+
let branch;
|
|
2590
|
+
if (requireBranch) {
|
|
2591
|
+
branch = getCurrentBranch();
|
|
2592
|
+
} else {
|
|
2593
|
+
try {
|
|
2594
|
+
branch = getCurrentBranch();
|
|
2595
|
+
} catch {
|
|
2596
|
+
branch = null;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2549
2599
|
const credential = await requirePat(context.org);
|
|
2550
2600
|
return {
|
|
2551
2601
|
context,
|
|
@@ -2562,15 +2612,15 @@ function createPrStatusCommand() {
|
|
|
2562
2612
|
try {
|
|
2563
2613
|
const resolved = await resolvePrCommandContext(options);
|
|
2564
2614
|
context = resolved.context;
|
|
2565
|
-
const
|
|
2615
|
+
const branch = resolved.branch;
|
|
2616
|
+
const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch);
|
|
2566
2617
|
const pullRequestsWithChecks = await Promise.all(
|
|
2567
2618
|
pullRequests.map(async (pullRequest) => ({
|
|
2568
2619
|
...pullRequest,
|
|
2569
2620
|
checks: await getPullRequestChecks(resolved.context, resolved.repo, resolved.pat, pullRequest.id)
|
|
2570
2621
|
}))
|
|
2571
2622
|
);
|
|
2572
|
-
const { branch, repo
|
|
2573
|
-
const result = { branch, repository: repo, pullRequests: pullRequestsWithChecks };
|
|
2623
|
+
const result = { branch, repository: resolved.repo, pullRequests: pullRequestsWithChecks };
|
|
2574
2624
|
if (options.json) {
|
|
2575
2625
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2576
2626
|
`);
|
|
@@ -2611,11 +2661,12 @@ function createPrOpenCommand() {
|
|
|
2611
2661
|
writeError("Pull request creation requires a source branch other than develop.");
|
|
2612
2662
|
return;
|
|
2613
2663
|
}
|
|
2664
|
+
const openBranch = resolved.branch;
|
|
2614
2665
|
const result = await openPullRequest(
|
|
2615
2666
|
resolved.context,
|
|
2616
2667
|
resolved.repo,
|
|
2617
2668
|
resolved.pat,
|
|
2618
|
-
|
|
2669
|
+
openBranch,
|
|
2619
2670
|
title,
|
|
2620
2671
|
description
|
|
2621
2672
|
);
|
|
@@ -2648,28 +2699,52 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
2648
2699
|
}
|
|
2649
2700
|
function createPrCommentsCommand() {
|
|
2650
2701
|
const command = new Command12("comments");
|
|
2651
|
-
command.description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--json", "output JSON").action(async (options) => {
|
|
2702
|
+
command.description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", "target the pull request with this numeric id, instead of the current branch's PR").option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--json", "output JSON").action(async (options) => {
|
|
2652
2703
|
validateOrgProjectPair(options);
|
|
2653
2704
|
let context;
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
});
|
|
2660
|
-
if (pullRequests.length === 0) {
|
|
2661
|
-
writeError(`No active pull request found for branch ${resolved.branch}.`);
|
|
2705
|
+
let explicitPrId = null;
|
|
2706
|
+
if (options.prNumber !== void 0) {
|
|
2707
|
+
explicitPrId = parsePositivePrNumber(options.prNumber);
|
|
2708
|
+
if (explicitPrId === null) {
|
|
2709
|
+
writeError(`Invalid --pr-number "${options.prNumber}"; expected a positive integer.`);
|
|
2662
2710
|
return;
|
|
2663
2711
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2712
|
+
}
|
|
2713
|
+
try {
|
|
2714
|
+
const resolved = await resolvePrCommandContext(options, { requireBranch: explicitPrId === null });
|
|
2715
|
+
context = resolved.context;
|
|
2716
|
+
let pullRequest;
|
|
2717
|
+
let branchLabel;
|
|
2718
|
+
if (explicitPrId !== null) {
|
|
2719
|
+
try {
|
|
2720
|
+
pullRequest = await getPullRequestById(resolved.context, resolved.repo, resolved.pat, explicitPrId);
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
|
|
2723
|
+
writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`);
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
throw err;
|
|
2727
|
+
}
|
|
2728
|
+
branchLabel = resolved.branch ?? pullRequest.sourceRefName;
|
|
2729
|
+
} else {
|
|
2730
|
+
const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
|
|
2731
|
+
status: "active"
|
|
2732
|
+
});
|
|
2733
|
+
if (pullRequests.length === 0) {
|
|
2734
|
+
writeError(`No active pull request found for branch ${resolved.branch}.`);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (pullRequests.length > 1) {
|
|
2738
|
+
const ids = pullRequests.map((pr) => `#${pr.id}`).join(", ");
|
|
2739
|
+
writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
pullRequest = pullRequests[0];
|
|
2743
|
+
branchLabel = resolved.branch;
|
|
2668
2744
|
}
|
|
2669
|
-
const pullRequest = pullRequests[0];
|
|
2670
2745
|
const allThreads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
|
|
2671
2746
|
const threads = options.hideResolved ? allThreads.filter((thread) => !isThreadResolved(thread.status)) : allThreads;
|
|
2672
|
-
const result = { branch:
|
|
2747
|
+
const result = { branch: branchLabel, pullRequest, threads };
|
|
2673
2748
|
if (options.json) {
|
|
2674
2749
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2675
2750
|
`);
|
|
@@ -2688,12 +2763,131 @@ function createPrCommentsCommand() {
|
|
|
2688
2763
|
});
|
|
2689
2764
|
return command;
|
|
2690
2765
|
}
|
|
2766
|
+
async function resolveThreadTarget(threadIdRaw, options) {
|
|
2767
|
+
validateOrgProjectPair(options);
|
|
2768
|
+
const threadId = parsePositivePrNumber(threadIdRaw);
|
|
2769
|
+
if (threadId === null) {
|
|
2770
|
+
writeError(`Invalid thread id "${threadIdRaw}"; expected a positive integer.`);
|
|
2771
|
+
return null;
|
|
2772
|
+
}
|
|
2773
|
+
let explicitPrId = null;
|
|
2774
|
+
if (options.prNumber !== void 0) {
|
|
2775
|
+
explicitPrId = parsePositivePrNumber(options.prNumber);
|
|
2776
|
+
if (explicitPrId === null) {
|
|
2777
|
+
writeError(`Invalid --pr-number "${options.prNumber}"; expected a positive integer.`);
|
|
2778
|
+
return null;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
const resolved = await resolvePrCommandContext(options, { requireBranch: explicitPrId === null });
|
|
2782
|
+
let pullRequest;
|
|
2783
|
+
if (explicitPrId !== null) {
|
|
2784
|
+
try {
|
|
2785
|
+
pullRequest = await getPullRequestById(resolved.context, resolved.repo, resolved.pat, explicitPrId);
|
|
2786
|
+
} catch (err) {
|
|
2787
|
+
if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
|
|
2788
|
+
writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`);
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
throw err;
|
|
2792
|
+
}
|
|
2793
|
+
} else {
|
|
2794
|
+
const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
|
|
2795
|
+
status: "active"
|
|
2796
|
+
});
|
|
2797
|
+
if (pullRequests.length === 0) {
|
|
2798
|
+
writeError(`No active pull request found for branch ${resolved.branch}.`);
|
|
2799
|
+
return null;
|
|
2800
|
+
}
|
|
2801
|
+
if (pullRequests.length > 1) {
|
|
2802
|
+
const ids = pullRequests.map((pr) => `#${pr.id}`).join(", ");
|
|
2803
|
+
writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
|
|
2804
|
+
return null;
|
|
2805
|
+
}
|
|
2806
|
+
pullRequest = pullRequests[0];
|
|
2807
|
+
}
|
|
2808
|
+
return { context: resolved.context, repo: resolved.repo, pat: resolved.pat, pullRequest, threadId };
|
|
2809
|
+
}
|
|
2810
|
+
async function runThreadStateChange(threadIdRaw, options, direction) {
|
|
2811
|
+
let context;
|
|
2812
|
+
try {
|
|
2813
|
+
const target = await resolveThreadTarget(threadIdRaw, options);
|
|
2814
|
+
if (target === null) {
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
context = target.context;
|
|
2818
|
+
const threads = await getPullRequestThreads(target.context, target.repo, target.pat, target.pullRequest.id);
|
|
2819
|
+
const thread = threads.find((t) => t.id === target.threadId);
|
|
2820
|
+
if (!thread) {
|
|
2821
|
+
writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`);
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
const alreadyInTargetState = direction === "resolve" ? isThreadResolved(thread.status) : !isThreadResolved(thread.status);
|
|
2825
|
+
const targetStatus = direction === "resolve" ? "fixed" : "active";
|
|
2826
|
+
if (alreadyInTargetState) {
|
|
2827
|
+
const humanLabel = direction === "resolve" ? "resolved" : "active";
|
|
2828
|
+
const noopResult = {
|
|
2829
|
+
pullRequestId: target.pullRequest.id,
|
|
2830
|
+
threadId: target.threadId,
|
|
2831
|
+
status: targetStatus,
|
|
2832
|
+
noop: true
|
|
2833
|
+
};
|
|
2834
|
+
if (options.json) {
|
|
2835
|
+
process.stdout.write(`${JSON.stringify(noopResult, null, 2)}
|
|
2836
|
+
`);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
process.stdout.write(`Thread #${target.threadId} is already ${humanLabel} on pull request #${target.pullRequest.id}.
|
|
2840
|
+
`);
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
const updated = await patchThreadStatus(
|
|
2844
|
+
target.context,
|
|
2845
|
+
target.repo,
|
|
2846
|
+
target.pat,
|
|
2847
|
+
target.pullRequest.id,
|
|
2848
|
+
target.threadId,
|
|
2849
|
+
targetStatus
|
|
2850
|
+
);
|
|
2851
|
+
const result = {
|
|
2852
|
+
pullRequestId: target.pullRequest.id,
|
|
2853
|
+
threadId: target.threadId,
|
|
2854
|
+
status: targetStatus,
|
|
2855
|
+
noop: false
|
|
2856
|
+
};
|
|
2857
|
+
if (options.json) {
|
|
2858
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2859
|
+
`);
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
const verb = direction === "resolve" ? "resolved" : "reopened";
|
|
2863
|
+
process.stdout.write(`Thread #${target.threadId} ${verb} on pull request #${target.pullRequest.id} (status: ${updated.status}).
|
|
2864
|
+
`);
|
|
2865
|
+
} catch (err) {
|
|
2866
|
+
handlePrCommandError(err, context, "write");
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
function createPrCommentResolveCommand() {
|
|
2870
|
+
const command = new Command12("comment-resolve");
|
|
2871
|
+
command.description("Mark a pull request comment thread as resolved").argument("<threadId>", "numeric id of the thread to resolve").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", "target the pull request with this numeric id, instead of the current branch's PR").option("--json", "output JSON").action(async (threadIdRaw, options) => {
|
|
2872
|
+
await runThreadStateChange(threadIdRaw, options, "resolve");
|
|
2873
|
+
});
|
|
2874
|
+
return command;
|
|
2875
|
+
}
|
|
2876
|
+
function createPrCommentReopenCommand() {
|
|
2877
|
+
const command = new Command12("comment-reopen");
|
|
2878
|
+
command.description("Reopen (set to active) a previously resolved pull request comment thread").argument("<threadId>", "numeric id of the thread to reopen").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", "target the pull request with this numeric id, instead of the current branch's PR").option("--json", "output JSON").action(async (threadIdRaw, options) => {
|
|
2879
|
+
await runThreadStateChange(threadIdRaw, options, "reopen");
|
|
2880
|
+
});
|
|
2881
|
+
return command;
|
|
2882
|
+
}
|
|
2691
2883
|
function createPrCommand() {
|
|
2692
2884
|
const command = new Command12("pr");
|
|
2693
2885
|
command.description("Manage Azure DevOps pull requests");
|
|
2694
2886
|
command.addCommand(createPrStatusCommand());
|
|
2695
2887
|
command.addCommand(createPrOpenCommand());
|
|
2696
2888
|
command.addCommand(createPrCommentsCommand());
|
|
2889
|
+
command.addCommand(createPrCommentResolveCommand());
|
|
2890
|
+
command.addCommand(createPrCommentReopenCommand());
|
|
2697
2891
|
return command;
|
|
2698
2892
|
}
|
|
2699
2893
|
|