azdo-cli 0.22.0-develop.787 → 0.22.0-develop.793
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/README.md +7 -3
- package/dist/index.js +129 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
15
15
|
- Attach a local file to a work item, or remove a named attachment (`add-attachment`, `delete-attachment`)
|
|
16
16
|
- Read/write rich-text fields as markdown (`get-md-field`, `set-md-field`)
|
|
17
17
|
- Download images embedded in rich-text fields, optionally resized for LLM use (`get-item`/`get-md-field` `--download-images`, `--resize-images`)
|
|
18
|
-
-
|
|
18
|
+
- List pull requests with their draft state, dates, reviewers, labels and linked work item ids (`pr list --work-items`), check branch pull request status, open PRs to `develop` (optionally pre-filled from a repository-defined template), update an existing PR's title or description (`pr update`), abandon or reactivate one (`pr abandon` / `pr reactivate`), list PR comment threads for any PR (`--pr-number`), resolve/reopen threads, link/unlink work items, and add/remove required or optional reviewers — all from the CLI (`pr`)
|
|
19
19
|
- Feed long PR titles, descriptions and comment bodies from a file or a pipe instead of the shell (`--title-file`, `--description-file`, `--file`; `-` reads standard input)
|
|
20
20
|
- Persist org/project/default fields in local config (`config`)
|
|
21
21
|
- List all fields of a work item (`list-fields`)
|
|
@@ -67,9 +67,11 @@ azdo delete-attachment 12345 screenshot.png --yes # skip the prompt (scr
|
|
|
67
67
|
azdo delete-attachment 12345 screenshot.png --id <guid> # disambiguate when the name is shared
|
|
68
68
|
|
|
69
69
|
# Find a pull request — one API call, any branch
|
|
70
|
-
azdo pr list # active PRs in the repository
|
|
71
|
-
azdo pr list --branch feature/x --json # id, title, source/target, author, url, description
|
|
70
|
+
azdo pr list # active PRs in the repository ([active, draft] marks a draft)
|
|
71
|
+
azdo pr list --branch feature/x --json # id, title, source/target, author, url, description, isDraft,
|
|
72
|
+
# creationDate, closedDate, reviewers (uniqueName, vote), labels
|
|
72
73
|
azdo pr list --status all --top 50
|
|
74
|
+
azdo pr list --work-items --json # + workItemIds per PR: the whole PR ↔ work item map in one call
|
|
73
75
|
|
|
74
76
|
# PR comment threads — list, filter, target by number, resolve or reopen
|
|
75
77
|
azdo pr comments # active-branch PR; code-anchored threads show file:line
|
|
@@ -121,6 +123,8 @@ azdo pr work-items unlink 1234 --pr-number 64
|
|
|
121
123
|
azdo pr reviewers add jane@example.com --pr-number 64 # optional by default
|
|
122
124
|
azdo pr reviewers add jane@example.com --pr-number 64 --required # required (or promotes in place)
|
|
123
125
|
azdo pr reviewers remove jane@example.com --pr-number 64
|
|
126
|
+
azdo pr reviewers list --pr-number 64 # who must review, and how each voted
|
|
127
|
+
azdo pr reviewers list --pr-number 64 --json # { pullRequestId, reviewers: [{ id, uniqueName, isRequired, vote, voteState, ... }] }
|
|
124
128
|
|
|
125
129
|
# Any pr subcommand can target another repository
|
|
126
130
|
azdo pr comments --repo other-repo --pr-number 12
|
package/dist/index.js
CHANGED
|
@@ -3704,7 +3704,12 @@ function mapPullRequest(context, repo, pullRequest) {
|
|
|
3704
3704
|
url: pullRequest._links?.web?.href ?? buildPullRequestWebUrl(context, repo, pullRequest.pullRequestId),
|
|
3705
3705
|
description: pullRequest.description?.trim() || null,
|
|
3706
3706
|
createdByUniqueName: pullRequest.createdBy?.uniqueName ?? null,
|
|
3707
|
-
createdById: pullRequest.createdBy?.id ?? null
|
|
3707
|
+
createdById: pullRequest.createdBy?.id ?? null,
|
|
3708
|
+
isDraft: pullRequest.isDraft ?? false,
|
|
3709
|
+
creationDate: pullRequest.creationDate ?? null,
|
|
3710
|
+
closedDate: pullRequest.closedDate ?? null,
|
|
3711
|
+
reviewers: (pullRequest.reviewers ?? []).map(mapReviewer),
|
|
3712
|
+
labels: (pullRequest.labels ?? []).filter((label) => label.active !== false).map((label) => label.name)
|
|
3708
3713
|
};
|
|
3709
3714
|
}
|
|
3710
3715
|
function mapPullRequestCheckName(status2) {
|
|
@@ -3884,6 +3889,15 @@ async function listRepositoryPullRequests(context, repo, cred, opts) {
|
|
|
3884
3889
|
const data = await readJsonResponse(response);
|
|
3885
3890
|
return data.value.map((pullRequest) => mapPullRequest(context, repo, pullRequest));
|
|
3886
3891
|
}
|
|
3892
|
+
async function getPullRequestWorkItemIds(context, repo, cred, prId) {
|
|
3893
|
+
const url = new URL(
|
|
3894
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/workitems`
|
|
3895
|
+
);
|
|
3896
|
+
url.searchParams.set("api-version", "7.1");
|
|
3897
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3898
|
+
const data = await readJsonResponse(response);
|
|
3899
|
+
return data.value.map((ref) => Number.parseInt(ref.id, 10)).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
|
|
3900
|
+
}
|
|
3887
3901
|
async function getPullRequestChecks(context, repo, cred, prId) {
|
|
3888
3902
|
const response = await fetchWithErrors(
|
|
3889
3903
|
buildPullRequestStatusesUrl(context, repo, prId).toString(),
|
|
@@ -4247,13 +4261,34 @@ function buildPullRequestReviewerUrl(context, repo, prId, reviewerId) {
|
|
|
4247
4261
|
url.searchParams.set("api-version", "7.1");
|
|
4248
4262
|
return url;
|
|
4249
4263
|
}
|
|
4264
|
+
function reviewerVoteState(vote) {
|
|
4265
|
+
switch (vote) {
|
|
4266
|
+
case 10:
|
|
4267
|
+
return "approved";
|
|
4268
|
+
case 5:
|
|
4269
|
+
return "approved-with-suggestions";
|
|
4270
|
+
case 0:
|
|
4271
|
+
return "no-vote";
|
|
4272
|
+
case -5:
|
|
4273
|
+
return "waiting-for-author";
|
|
4274
|
+
case -10:
|
|
4275
|
+
return "rejected";
|
|
4276
|
+
case 15:
|
|
4277
|
+
return "bypassed";
|
|
4278
|
+
default:
|
|
4279
|
+
return "unknown";
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4250
4282
|
function mapReviewer(data) {
|
|
4283
|
+
const vote = data.vote ?? 0;
|
|
4251
4284
|
return {
|
|
4252
4285
|
id: data.id,
|
|
4253
4286
|
displayName: data.displayName ?? null,
|
|
4254
4287
|
uniqueName: data.uniqueName ?? null,
|
|
4255
4288
|
isRequired: data.isRequired ?? false,
|
|
4256
|
-
vote
|
|
4289
|
+
vote,
|
|
4290
|
+
voteState: reviewerVoteState(vote),
|
|
4291
|
+
hasDeclined: data.hasDeclined ?? false
|
|
4257
4292
|
};
|
|
4258
4293
|
}
|
|
4259
4294
|
async function getPullRequestReviewers(context, repo, cred, prId) {
|
|
@@ -5811,42 +5846,73 @@ function createPrCommentDeleteCommand() {
|
|
|
5811
5846
|
}
|
|
5812
5847
|
var LIST_STATUS_VALUES = ["active", "completed", "abandoned", "all"];
|
|
5813
5848
|
var DEFAULT_LIST_TOP = 25;
|
|
5849
|
+
var WORK_ITEM_LOOKUP_CONCURRENCY = 5;
|
|
5814
5850
|
function formatPullRequestListEntry(pullRequest) {
|
|
5815
|
-
|
|
5816
|
-
|
|
5851
|
+
const state = pullRequest.isDraft ? `${pullRequest.status}, draft` : pullRequest.status;
|
|
5852
|
+
const lines = [
|
|
5853
|
+
`#${pullRequest.id} [${state}] ${pullRequest.title}`,
|
|
5817
5854
|
` ${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
|
|
5818
|
-
` Author: ${pullRequest.createdBy ?? "Unknown"}
|
|
5819
|
-
|
|
5820
|
-
|
|
5855
|
+
` Author: ${pullRequest.createdBy ?? "Unknown"}`
|
|
5856
|
+
];
|
|
5857
|
+
if ("workItemIds" in pullRequest) {
|
|
5858
|
+
const ids = pullRequest.workItemIds.map((id) => `#${id}`).join(", ");
|
|
5859
|
+
lines.push(` Work items: ${ids || "none"}`);
|
|
5860
|
+
}
|
|
5861
|
+
lines.push(` ${pullRequest.url ?? "\u2014"}`);
|
|
5862
|
+
return lines.join("\n");
|
|
5863
|
+
}
|
|
5864
|
+
function parseListOptions(options) {
|
|
5865
|
+
const status2 = options.status ?? "active";
|
|
5866
|
+
if (!LIST_STATUS_VALUES.includes(status2)) {
|
|
5867
|
+
writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
|
|
5868
|
+
return null;
|
|
5869
|
+
}
|
|
5870
|
+
let top = DEFAULT_LIST_TOP;
|
|
5871
|
+
if (options.top !== void 0) {
|
|
5872
|
+
const parsed = parsePositivePrNumber(options.top);
|
|
5873
|
+
if (parsed === null) {
|
|
5874
|
+
writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
|
|
5875
|
+
return null;
|
|
5876
|
+
}
|
|
5877
|
+
top = parsed;
|
|
5878
|
+
}
|
|
5879
|
+
const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
|
|
5880
|
+
return { status: status2, top, branch };
|
|
5881
|
+
}
|
|
5882
|
+
async function attachWorkItemIds(context, repo, cred, pullRequests) {
|
|
5883
|
+
const result = new Array(pullRequests.length);
|
|
5884
|
+
let next = 0;
|
|
5885
|
+
const worker = async () => {
|
|
5886
|
+
while (next < pullRequests.length) {
|
|
5887
|
+
const index = next;
|
|
5888
|
+
next += 1;
|
|
5889
|
+
const pullRequest = pullRequests[index];
|
|
5890
|
+
const workItemIds = await getPullRequestWorkItemIds(context, repo, cred, pullRequest.id);
|
|
5891
|
+
result[index] = { ...pullRequest, workItemIds };
|
|
5892
|
+
}
|
|
5893
|
+
};
|
|
5894
|
+
await Promise.all(Array.from({ length: Math.min(WORK_ITEM_LOOKUP_CONCURRENCY, pullRequests.length) }, worker));
|
|
5895
|
+
return result;
|
|
5821
5896
|
}
|
|
5822
5897
|
function createPrListCommand() {
|
|
5823
5898
|
const command = new Command12("list");
|
|
5824
|
-
withCommonPrOptions(configureUnwrappedHelp(command)).description("List pull requests in the repository, optionally filtered by source branch").option("--branch <name>", "only pull requests whose source branch is this one (with or without the refs/heads/ prefix)").option("--status <status>", `pull request status filter (${LIST_STATUS_VALUES.join(" | ")})`, "active").option("--top <N>", `maximum number of pull requests to return (default ${DEFAULT_LIST_TOP})`).option("--json", "output JSON").action(async (options) => {
|
|
5899
|
+
withCommonPrOptions(configureUnwrappedHelp(command)).description("List pull requests in the repository, optionally filtered by source branch").option("--branch <name>", "only pull requests whose source branch is this one (with or without the refs/heads/ prefix)").option("--status <status>", `pull request status filter (${LIST_STATUS_VALUES.join(" | ")})`, "active").option("--top <N>", `maximum number of pull requests to return (default ${DEFAULT_LIST_TOP})`).option("--work-items", "also return the ids of each pull request's linked work items (one extra API call per pull request)").option("--json", "output JSON").action(async (options) => {
|
|
5825
5900
|
validateOrgProjectPair(options);
|
|
5826
|
-
const
|
|
5827
|
-
if (
|
|
5828
|
-
writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
|
|
5901
|
+
const parsed = parseListOptions(options);
|
|
5902
|
+
if (parsed === null) {
|
|
5829
5903
|
return;
|
|
5830
5904
|
}
|
|
5831
|
-
|
|
5832
|
-
if (options.top !== void 0) {
|
|
5833
|
-
const parsed = parsePositivePrNumber(options.top);
|
|
5834
|
-
if (parsed === null) {
|
|
5835
|
-
writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
|
|
5836
|
-
return;
|
|
5837
|
-
}
|
|
5838
|
-
top = parsed;
|
|
5839
|
-
}
|
|
5840
|
-
const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
|
|
5905
|
+
const { status: status2, top, branch } = parsed;
|
|
5841
5906
|
let context;
|
|
5842
5907
|
try {
|
|
5843
5908
|
const resolved = await resolvePrCommandContext(options, { requireBranch: false });
|
|
5844
5909
|
context = resolved.context;
|
|
5845
|
-
const
|
|
5910
|
+
const listed = await listRepositoryPullRequests(resolved.context, resolved.repo, resolved.pat, {
|
|
5846
5911
|
sourceBranch: branch ?? void 0,
|
|
5847
5912
|
status: status2,
|
|
5848
5913
|
top
|
|
5849
5914
|
});
|
|
5915
|
+
const pullRequests = options.workItems ? await attachWorkItemIds(resolved.context, resolved.repo, resolved.pat, listed) : listed;
|
|
5850
5916
|
const result = {
|
|
5851
5917
|
repository: resolved.repo,
|
|
5852
5918
|
branch,
|
|
@@ -6042,9 +6108,50 @@ async function runReviewerRemove(reviewer, options) {
|
|
|
6042
6108
|
handlePrCommandError(err, context, "write");
|
|
6043
6109
|
}
|
|
6044
6110
|
}
|
|
6111
|
+
function formatReviewerLine(reviewer) {
|
|
6112
|
+
const name = reviewer.displayName ?? reviewer.uniqueName ?? reviewer.id;
|
|
6113
|
+
const unique = reviewer.uniqueName && reviewer.uniqueName !== name ? ` <${reviewer.uniqueName}>` : "";
|
|
6114
|
+
const kind = reviewer.isRequired ? "required" : "optional";
|
|
6115
|
+
const declined = reviewer.hasDeclined ? ", declined" : "";
|
|
6116
|
+
return `${name}${unique} \u2014 ${reviewer.voteState} (${kind}${declined})`;
|
|
6117
|
+
}
|
|
6118
|
+
async function runReviewerList(options) {
|
|
6119
|
+
let context;
|
|
6120
|
+
try {
|
|
6121
|
+
const target = await resolvePullRequestTarget(options, {
|
|
6122
|
+
onContextResolved: (resolved) => {
|
|
6123
|
+
context = resolved;
|
|
6124
|
+
}
|
|
6125
|
+
});
|
|
6126
|
+
if (target === null) {
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
const reviewers = await getPullRequestReviewers(target.context, target.repo, target.pat, target.pullRequest.id);
|
|
6130
|
+
const result = { pullRequestId: target.pullRequest.id, reviewers };
|
|
6131
|
+
if (options.json) {
|
|
6132
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6133
|
+
`);
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
if (reviewers.length === 0) {
|
|
6137
|
+
process.stdout.write(`No reviewers on pull request #${target.pullRequest.id}.
|
|
6138
|
+
`);
|
|
6139
|
+
return;
|
|
6140
|
+
}
|
|
6141
|
+
process.stdout.write(`${reviewers.map(formatReviewerLine).join("\n")}
|
|
6142
|
+
`);
|
|
6143
|
+
} catch (err) {
|
|
6144
|
+
handlePrCommandError(err, context, "read");
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6045
6147
|
function createPrReviewersCommand() {
|
|
6046
6148
|
const command = new Command12("reviewers");
|
|
6047
6149
|
command.description("Manage pull request reviewers");
|
|
6150
|
+
const list = new Command12("list");
|
|
6151
|
+
withCommonPrOptions(configureUnwrappedHelp(list)).description("List the pull request reviewers with their votes (approved, waiting-for-author, \u2026) and required flag").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (_options, command2) => {
|
|
6152
|
+
await runReviewerList(mergedPrOptions(command2));
|
|
6153
|
+
});
|
|
6154
|
+
command.addCommand(list);
|
|
6048
6155
|
const add = new Command12("add");
|
|
6049
6156
|
withCommonPrOptions(configureUnwrappedHelp(add)).description("Add a reviewer to the pull request (optional by default)").argument("<reviewer>", "reviewer email or unique name").option("--required", "mark the reviewer as required instead of optional").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (reviewer, _options, command2) => {
|
|
6050
6157
|
await runReviewerAdd(reviewer, mergedPrOptions(command2));
|