azdo-cli 0.22.0-develop.822 → 0.22.0-develop.828
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 +1 -0
- package/dist/index.js +320 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ git log -1 --format=%B | azdo pr comments add --file - --pr-number 64 # "-"
|
|
|
105
105
|
azdo pr open --title "Fix the thing" --description "Because X was broken"
|
|
106
106
|
azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
|
|
107
107
|
azdo pr open --title "Fix the thing" --description-file body.md # or --description-file - to pipe it in
|
|
108
|
+
azdo pr open --title "Fix the thing" --target master --source feature/x --draft --work-item 1234 --label bug # --work-item/--label repeat; --json adds top-level id and url
|
|
108
109
|
# The description plus the template must stay within Azure DevOps' 4000-character cap; over it,
|
|
109
110
|
# the command says by how much and creates nothing. See docs/commands.md.
|
|
110
111
|
|
package/dist/index.js
CHANGED
|
@@ -143,6 +143,7 @@ function getActiveTraceWriter() {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// src/services/command-helpers.ts
|
|
146
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
146
147
|
var EXIT_CREDENTIAL_STORE_UNAVAILABLE = 4;
|
|
147
148
|
function reportCredentialStoreUnavailable(err) {
|
|
148
149
|
if (!(err instanceof CredentialStoreUnavailableError)) return false;
|
|
@@ -279,11 +280,29 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
279
280
|
process.exitCode = 1;
|
|
280
281
|
}
|
|
281
282
|
}
|
|
283
|
+
var STDIN_PATH = "-";
|
|
284
|
+
function readTextSource(file) {
|
|
285
|
+
if (file === STDIN_PATH) {
|
|
286
|
+
try {
|
|
287
|
+
return readFileSync2(0, "utf-8");
|
|
288
|
+
} catch {
|
|
289
|
+
throw new Error("Cannot read standard input.");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!existsSync(file)) {
|
|
293
|
+
throw new Error(`File not found: ${file}`);
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
return readFileSync2(file, "utf-8");
|
|
297
|
+
} catch {
|
|
298
|
+
throw new Error(`Cannot read file: ${file}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
282
301
|
|
|
283
302
|
// src/services/image-download.ts
|
|
284
303
|
import { Jimp } from "jimp";
|
|
285
304
|
import { writeFile } from "fs/promises";
|
|
286
|
-
import { existsSync } from "fs";
|
|
305
|
+
import { existsSync as existsSync2 } from "fs";
|
|
287
306
|
import { tmpdir } from "os";
|
|
288
307
|
import { join } from "path";
|
|
289
308
|
var ATTACHMENT_GUID_RE = /_apis\/wit\/attachments\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/;
|
|
@@ -366,7 +385,7 @@ function resolveImageDownloadOptions(flags) {
|
|
|
366
385
|
maxWidth = parsed;
|
|
367
386
|
}
|
|
368
387
|
const outputDir = flags.imagesPath ?? tmpdir();
|
|
369
|
-
if (flags.imagesPath !== void 0 && !
|
|
388
|
+
if (flags.imagesPath !== void 0 && !existsSync2(outputDir)) {
|
|
370
389
|
throw new Error(`Images path "${outputDir}" does not exist.`);
|
|
371
390
|
}
|
|
372
391
|
return { enabled, maxWidth, outputDir };
|
|
@@ -668,6 +687,11 @@ function buildWorkItemCommentsUrl(context, id) {
|
|
|
668
687
|
url.searchParams.set("api-version", "7.1-preview.4");
|
|
669
688
|
return url;
|
|
670
689
|
}
|
|
690
|
+
function buildWorkItemCommentUrl(context, id, commentId) {
|
|
691
|
+
const url = buildWorkItemCommentsUrl(context, id);
|
|
692
|
+
url.pathname = `${url.pathname}/${commentId}`;
|
|
693
|
+
return url;
|
|
694
|
+
}
|
|
671
695
|
function mapWorkItemComment(comment, fallbackWorkItemId) {
|
|
672
696
|
return {
|
|
673
697
|
id: comment.id ?? comment.commentId ?? 0,
|
|
@@ -892,16 +916,11 @@ async function listWorkItemComments(context, id, cred) {
|
|
|
892
916
|
comments
|
|
893
917
|
};
|
|
894
918
|
}
|
|
895
|
-
async function
|
|
896
|
-
const url = buildWorkItemCommentsUrl(context, id);
|
|
897
|
-
url.searchParams.set("format", format);
|
|
919
|
+
async function sendCommentRequest(url, cred, method, body) {
|
|
898
920
|
const response = await fetchWithErrors(url.toString(), {
|
|
899
|
-
method
|
|
900
|
-
headers: {
|
|
901
|
-
|
|
902
|
-
"Content-Type": "application/json"
|
|
903
|
-
},
|
|
904
|
-
body: JSON.stringify({ text })
|
|
921
|
+
method,
|
|
922
|
+
headers: body === void 0 ? authHeaders(cred) : { ...authHeaders(cred), "Content-Type": "application/json" },
|
|
923
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
905
924
|
});
|
|
906
925
|
if (response.status === 400) {
|
|
907
926
|
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
@@ -910,16 +929,50 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
|
910
929
|
if (!response.ok) {
|
|
911
930
|
throw httpError(response);
|
|
912
931
|
}
|
|
932
|
+
return response;
|
|
933
|
+
}
|
|
934
|
+
async function writeCommentText(url, cred, method, text, format, fallback) {
|
|
935
|
+
url.searchParams.set("format", format);
|
|
936
|
+
const response = await sendCommentRequest(url, cred, method, { text });
|
|
913
937
|
const data = await response.json();
|
|
914
938
|
return {
|
|
915
|
-
workItemId: data.workItemId ??
|
|
916
|
-
commentId: data.commentId ?? data.id ??
|
|
939
|
+
workItemId: data.workItemId ?? fallback.workItemId,
|
|
940
|
+
commentId: data.commentId ?? data.id ?? fallback.commentId,
|
|
917
941
|
text: typeof data.text === "string" ? data.text : text,
|
|
918
942
|
author: data.createdBy?.displayName ?? null,
|
|
919
943
|
createdAt: data.createdDate ?? null,
|
|
944
|
+
modifiedAt: data.modifiedDate ?? null,
|
|
920
945
|
url: data.url ?? null
|
|
921
946
|
};
|
|
922
947
|
}
|
|
948
|
+
async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
949
|
+
const written = await writeCommentText(
|
|
950
|
+
buildWorkItemCommentsUrl(context, id),
|
|
951
|
+
cred,
|
|
952
|
+
"POST",
|
|
953
|
+
text,
|
|
954
|
+
format,
|
|
955
|
+
{ workItemId: id, commentId: 0 }
|
|
956
|
+
);
|
|
957
|
+
return {
|
|
958
|
+
workItemId: written.workItemId,
|
|
959
|
+
commentId: written.commentId,
|
|
960
|
+
text: written.text,
|
|
961
|
+
author: written.author,
|
|
962
|
+
createdAt: written.createdAt,
|
|
963
|
+
url: written.url
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
async function updateWorkItemComment(context, id, commentId, cred, text, format = "html") {
|
|
967
|
+
return writeCommentText(buildWorkItemCommentUrl(context, id, commentId), cred, "PATCH", text, format, {
|
|
968
|
+
workItemId: id,
|
|
969
|
+
commentId
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async function deleteWorkItemComment(context, id, commentId, cred) {
|
|
973
|
+
await sendCommentRequest(buildWorkItemCommentUrl(context, id, commentId), cred, "DELETE");
|
|
974
|
+
return { workItemId: id, commentId, deleted: true };
|
|
975
|
+
}
|
|
923
976
|
async function updateWorkItem(context, id, cred, fieldName, operations) {
|
|
924
977
|
const result = await applyWorkItemPatch(context, id, cred, operations);
|
|
925
978
|
const title = result.fields["System.Title"];
|
|
@@ -1093,7 +1146,7 @@ async function queryWorkItems(context, cred, filter) {
|
|
|
1093
1146
|
|
|
1094
1147
|
// src/services/auth.ts
|
|
1095
1148
|
import { createInterface } from "readline";
|
|
1096
|
-
import { existsSync as
|
|
1149
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1097
1150
|
import { dirname as dirname2, join as join2 } from "path";
|
|
1098
1151
|
|
|
1099
1152
|
// src/services/oauth-device-code.ts
|
|
@@ -1263,8 +1316,8 @@ async function promptForPat() {
|
|
|
1263
1316
|
});
|
|
1264
1317
|
}
|
|
1265
1318
|
function patFromEnvFile(envFile) {
|
|
1266
|
-
if (!
|
|
1267
|
-
const contents =
|
|
1319
|
+
if (!existsSync3(envFile)) return null;
|
|
1320
|
+
const contents = readFileSync3(envFile, "utf8");
|
|
1268
1321
|
for (const line of contents.split("\n")) {
|
|
1269
1322
|
const match = line.match(/^AZDO_PAT\s*=([^\n\r]+)$/);
|
|
1270
1323
|
if (match === null) continue;
|
|
@@ -3318,7 +3371,7 @@ function createGetMdFieldCommand() {
|
|
|
3318
3371
|
}
|
|
3319
3372
|
|
|
3320
3373
|
// src/commands/set-md-field.ts
|
|
3321
|
-
import { existsSync as
|
|
3374
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
3322
3375
|
import { Command as Command10 } from "commander";
|
|
3323
3376
|
function fail(message) {
|
|
3324
3377
|
process.stderr.write(`Error: ${message}
|
|
@@ -3338,11 +3391,11 @@ function resolveContent(inlineContent, options) {
|
|
|
3338
3391
|
return null;
|
|
3339
3392
|
}
|
|
3340
3393
|
function readFileContent(filePath) {
|
|
3341
|
-
if (!
|
|
3394
|
+
if (!existsSync4(filePath)) {
|
|
3342
3395
|
fail(`File not found: ${filePath}`);
|
|
3343
3396
|
}
|
|
3344
3397
|
try {
|
|
3345
|
-
return
|
|
3398
|
+
return readFileSync4(filePath, "utf-8");
|
|
3346
3399
|
} catch {
|
|
3347
3400
|
fail(`Cannot read file: ${filePath}`);
|
|
3348
3401
|
}
|
|
@@ -3407,7 +3460,7 @@ function createSetMdFieldCommand() {
|
|
|
3407
3460
|
}
|
|
3408
3461
|
|
|
3409
3462
|
// src/commands/upsert.ts
|
|
3410
|
-
import { existsSync as
|
|
3463
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
3411
3464
|
import { Command as Command11 } from "commander";
|
|
3412
3465
|
|
|
3413
3466
|
// src/services/task-document.ts
|
|
@@ -3571,12 +3624,12 @@ function loadSourceContent(options) {
|
|
|
3571
3624
|
return { content: options.content };
|
|
3572
3625
|
}
|
|
3573
3626
|
const filePath = options.file;
|
|
3574
|
-
if (!
|
|
3627
|
+
if (!existsSync5(filePath)) {
|
|
3575
3628
|
fail2(`File not found: ${filePath}`);
|
|
3576
3629
|
}
|
|
3577
3630
|
try {
|
|
3578
3631
|
return {
|
|
3579
|
-
content:
|
|
3632
|
+
content: readFileSync5(filePath, "utf-8"),
|
|
3580
3633
|
sourceFile: filePath
|
|
3581
3634
|
};
|
|
3582
3635
|
} catch {
|
|
@@ -3795,7 +3848,6 @@ function createListFieldsCommand() {
|
|
|
3795
3848
|
}
|
|
3796
3849
|
|
|
3797
3850
|
// src/commands/pr.ts
|
|
3798
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
3799
3851
|
import { Command as Command13 } from "commander";
|
|
3800
3852
|
|
|
3801
3853
|
// src/services/pr-client.ts
|
|
@@ -4130,43 +4182,26 @@ function formatContributions(composed) {
|
|
|
4130
4182
|
function describeDescriptionBudget(composed) {
|
|
4131
4183
|
return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
|
|
4132
4184
|
}
|
|
4133
|
-
|
|
4134
|
-
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
4135
|
-
status: "active",
|
|
4136
|
-
targetBranch: "develop"
|
|
4137
|
-
});
|
|
4138
|
-
if (existing.length === 1) {
|
|
4139
|
-
return {
|
|
4140
|
-
branch: sourceBranch,
|
|
4141
|
-
targetBranch: "develop",
|
|
4142
|
-
created: false,
|
|
4143
|
-
pullRequest: existing[0]
|
|
4144
|
-
};
|
|
4145
|
-
}
|
|
4146
|
-
if (existing.length > 1) {
|
|
4147
|
-
throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
|
|
4148
|
-
}
|
|
4149
|
-
const repository = await getRepository(context, repo, cred);
|
|
4150
|
-
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
4151
|
-
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
|
|
4152
|
-
const composed = composeDescription(description, template);
|
|
4153
|
-
if (composed === null) {
|
|
4154
|
-
throw new Error("DESCRIPTION_REQUIRED");
|
|
4155
|
-
}
|
|
4156
|
-
if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
|
|
4157
|
-
throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
|
|
4158
|
-
}
|
|
4185
|
+
function buildOpenPayload(sourceBranch, targetBranch, title, description, options) {
|
|
4159
4186
|
const payload = {
|
|
4160
4187
|
sourceRefName: `refs/heads/${sourceBranch}`,
|
|
4161
|
-
targetRefName:
|
|
4188
|
+
targetRefName: `refs/heads/${targetBranch}`,
|
|
4162
4189
|
title,
|
|
4163
|
-
description
|
|
4190
|
+
description
|
|
4164
4191
|
};
|
|
4192
|
+
if (options.isDraft) {
|
|
4193
|
+
payload.isDraft = true;
|
|
4194
|
+
}
|
|
4195
|
+
if (options.labels && options.labels.length > 0) {
|
|
4196
|
+
payload.labels = options.labels.map((name) => ({ name }));
|
|
4197
|
+
}
|
|
4198
|
+
return payload;
|
|
4199
|
+
}
|
|
4200
|
+
async function createPullRequest(context, repo, cred, payload, composed) {
|
|
4165
4201
|
const url = new URL(
|
|
4166
4202
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
4167
4203
|
);
|
|
4168
4204
|
url.searchParams.set("api-version", "7.1");
|
|
4169
|
-
let data;
|
|
4170
4205
|
try {
|
|
4171
4206
|
const response = await fetchWithErrors(url.toString(), {
|
|
4172
4207
|
method: "POST",
|
|
@@ -4176,19 +4211,69 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
4176
4211
|
},
|
|
4177
4212
|
body: JSON.stringify(payload)
|
|
4178
4213
|
});
|
|
4179
|
-
|
|
4214
|
+
return await readJsonResponse(response);
|
|
4180
4215
|
} catch (err) {
|
|
4181
4216
|
if (err instanceof Error && err.message.startsWith("HTTP_400")) {
|
|
4182
4217
|
throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
|
|
4183
4218
|
}
|
|
4184
4219
|
throw err;
|
|
4185
4220
|
}
|
|
4186
|
-
|
|
4221
|
+
}
|
|
4222
|
+
function linkWorkItems(context, repo, cred, prId, workItemIds) {
|
|
4223
|
+
return Promise.all(
|
|
4224
|
+
workItemIds.map(
|
|
4225
|
+
(id) => linkWorkItemToPullRequest(context, repo, cred, prId, id).then(
|
|
4226
|
+
() => ({ id, linked: true }),
|
|
4227
|
+
(err) => ({ id, linked: false, error: err instanceof Error ? err.message : String(err) })
|
|
4228
|
+
)
|
|
4229
|
+
)
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
async function openPullRequest(context, repo, cred, sourceBranch, title, description, options = {}) {
|
|
4233
|
+
const targetBranch = options.targetBranch ?? "develop";
|
|
4234
|
+
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
4235
|
+
status: "active",
|
|
4236
|
+
targetBranch
|
|
4237
|
+
});
|
|
4238
|
+
if (existing.length === 1) {
|
|
4239
|
+
return {
|
|
4240
|
+
branch: sourceBranch,
|
|
4241
|
+
targetBranch,
|
|
4242
|
+
created: false,
|
|
4243
|
+
pullRequest: existing[0]
|
|
4244
|
+
};
|
|
4245
|
+
}
|
|
4246
|
+
if (existing.length > 1) {
|
|
4247
|
+
throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest2) => pullRequest2.id).join(",")}`);
|
|
4248
|
+
}
|
|
4249
|
+
const repository = await getRepository(context, repo, cred);
|
|
4250
|
+
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
4251
|
+
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, targetBranch);
|
|
4252
|
+
const composed = composeDescription(description, template);
|
|
4253
|
+
if (composed === null) {
|
|
4254
|
+
throw new Error("DESCRIPTION_REQUIRED");
|
|
4255
|
+
}
|
|
4256
|
+
if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
|
|
4257
|
+
throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
|
|
4258
|
+
}
|
|
4259
|
+
const data = await createPullRequest(
|
|
4260
|
+
context,
|
|
4261
|
+
repo,
|
|
4262
|
+
cred,
|
|
4263
|
+
buildOpenPayload(sourceBranch, targetBranch, title, composed.text, options),
|
|
4264
|
+
composed
|
|
4265
|
+
);
|
|
4266
|
+
const pullRequest = mapPullRequest(context, repo, data);
|
|
4267
|
+
const result = {
|
|
4187
4268
|
branch: sourceBranch,
|
|
4188
|
-
targetBranch
|
|
4269
|
+
targetBranch,
|
|
4189
4270
|
created: true,
|
|
4190
|
-
pullRequest
|
|
4271
|
+
pullRequest
|
|
4191
4272
|
};
|
|
4273
|
+
if (options.workItemIds && options.workItemIds.length > 0) {
|
|
4274
|
+
result.workItems = await linkWorkItems(context, repo, cred, pullRequest.id, options.workItemIds);
|
|
4275
|
+
}
|
|
4276
|
+
return result;
|
|
4192
4277
|
}
|
|
4193
4278
|
async function updatePullRequest(context, repo, cred, prId, fields) {
|
|
4194
4279
|
if (fields.description !== void 0 && fields.description.length > MAX_PR_DESCRIPTION_CHARS) {
|
|
@@ -4615,24 +4700,11 @@ function writeContractError(line) {
|
|
|
4615
4700
|
process.exitCode = 1;
|
|
4616
4701
|
}
|
|
4617
4702
|
var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
|
|
4618
|
-
|
|
4619
|
-
function readTextSource(file) {
|
|
4620
|
-
if (file === STDIN_PATH) {
|
|
4621
|
-
try {
|
|
4622
|
-
return readFileSync5(0, "utf-8");
|
|
4623
|
-
} catch {
|
|
4624
|
-
writeError("Cannot read standard input.");
|
|
4625
|
-
return null;
|
|
4626
|
-
}
|
|
4627
|
-
}
|
|
4628
|
-
if (!existsSync5(file)) {
|
|
4629
|
-
writeError(`File not found: ${file}`);
|
|
4630
|
-
return null;
|
|
4631
|
-
}
|
|
4703
|
+
function readTextSource2(file) {
|
|
4632
4704
|
try {
|
|
4633
|
-
return
|
|
4634
|
-
} catch {
|
|
4635
|
-
writeError(
|
|
4705
|
+
return readTextSource(file);
|
|
4706
|
+
} catch (err) {
|
|
4707
|
+
writeError(err.message);
|
|
4636
4708
|
return null;
|
|
4637
4709
|
}
|
|
4638
4710
|
}
|
|
@@ -4643,7 +4715,7 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
|
4643
4715
|
}
|
|
4644
4716
|
let body;
|
|
4645
4717
|
if (file !== void 0) {
|
|
4646
|
-
const read =
|
|
4718
|
+
const read = readTextSource2(file);
|
|
4647
4719
|
if (read === null) {
|
|
4648
4720
|
return null;
|
|
4649
4721
|
}
|
|
@@ -4673,7 +4745,7 @@ function resolveOptionalTextInput(inline, file, flag) {
|
|
|
4673
4745
|
const emptyMessage = `${label} must not be empty. Pass the text inline or use --${flag}-file <path>.`;
|
|
4674
4746
|
let value;
|
|
4675
4747
|
if (file !== void 0) {
|
|
4676
|
-
const read =
|
|
4748
|
+
const read = readTextSource2(file);
|
|
4677
4749
|
if (read === null) {
|
|
4678
4750
|
return null;
|
|
4679
4751
|
}
|
|
@@ -4970,11 +5042,11 @@ function createPrStatusCommand() {
|
|
|
4970
5042
|
});
|
|
4971
5043
|
return command;
|
|
4972
5044
|
}
|
|
4973
|
-
function handlePrOpenError(err, context) {
|
|
5045
|
+
function handlePrOpenError(err, context, targetBranch = "develop") {
|
|
4974
5046
|
const message = err instanceof Error ? err.message : "";
|
|
4975
5047
|
if (message.startsWith("AMBIGUOUS_PRS:")) {
|
|
4976
5048
|
const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
4977
|
-
writeError(`Multiple active pull requests already exist for this branch targeting
|
|
5049
|
+
writeError(`Multiple active pull requests already exist for this branch targeting ${targetBranch}: ${ids}. Use pr status to review them.`);
|
|
4978
5050
|
return;
|
|
4979
5051
|
}
|
|
4980
5052
|
if (message === "DESCRIPTION_REQUIRED") {
|
|
@@ -4987,13 +5059,54 @@ function handlePrOpenError(err, context) {
|
|
|
4987
5059
|
}
|
|
4988
5060
|
handlePrCommandError(err, context, "write");
|
|
4989
5061
|
}
|
|
5062
|
+
function collectOption(value, previous) {
|
|
5063
|
+
return [...previous, value];
|
|
5064
|
+
}
|
|
5065
|
+
function stripHeadsPrefix(branch) {
|
|
5066
|
+
return branch.trim().replace(/^refs\/heads\//, "");
|
|
5067
|
+
}
|
|
5068
|
+
function resolveOpenOptions(options) {
|
|
5069
|
+
const targetBranch = options.target === void 0 ? "develop" : stripHeadsPrefix(options.target);
|
|
5070
|
+
const source = options.source === void 0 ? void 0 : stripHeadsPrefix(options.source);
|
|
5071
|
+
if (!targetBranch) {
|
|
5072
|
+
writeError("--target must not be empty.");
|
|
5073
|
+
return null;
|
|
5074
|
+
}
|
|
5075
|
+
if (source !== void 0 && !source) {
|
|
5076
|
+
writeError("--source must not be empty.");
|
|
5077
|
+
return null;
|
|
5078
|
+
}
|
|
5079
|
+
const workItemIds = [];
|
|
5080
|
+
for (const raw of options.workItem ?? []) {
|
|
5081
|
+
const id = Number(raw.trim());
|
|
5082
|
+
if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(id) || id <= 0) {
|
|
5083
|
+
writeError(`--work-item must be a positive integer, got "${raw}".`);
|
|
5084
|
+
return null;
|
|
5085
|
+
}
|
|
5086
|
+
if (!workItemIds.includes(id)) {
|
|
5087
|
+
workItemIds.push(id);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
const labels = [];
|
|
5091
|
+
for (const raw of options.label ?? []) {
|
|
5092
|
+
const name = raw.trim();
|
|
5093
|
+
if (!name) {
|
|
5094
|
+
writeError("--label must not be empty.");
|
|
5095
|
+
return null;
|
|
5096
|
+
}
|
|
5097
|
+
if (!labels.includes(name)) {
|
|
5098
|
+
labels.push(name);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
return { targetBranch, source, isDraft: options.draft === true, labels, workItemIds };
|
|
5102
|
+
}
|
|
4990
5103
|
function resolveOpenDescription(inline, file) {
|
|
4991
5104
|
if (inline !== void 0 && file !== void 0) {
|
|
4992
5105
|
writeError("Cannot specify both --description and --description-file.");
|
|
4993
5106
|
return null;
|
|
4994
5107
|
}
|
|
4995
5108
|
if (file !== void 0) {
|
|
4996
|
-
const fromFile =
|
|
5109
|
+
const fromFile = readTextSource2(file);
|
|
4997
5110
|
if (fromFile === null) {
|
|
4998
5111
|
return null;
|
|
4999
5112
|
}
|
|
@@ -5012,13 +5125,18 @@ function resolveOpenDescription(inline, file) {
|
|
|
5012
5125
|
}
|
|
5013
5126
|
function createPrOpenCommand() {
|
|
5014
5127
|
const command = new Command13("open");
|
|
5015
|
-
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
5128
|
+
withCommonPrOptions(command).description("Open a pull request (default: from the current branch to develop)").option("--title <title>", "pull request title").option(
|
|
5016
5129
|
"--description <description>",
|
|
5017
5130
|
"pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
|
|
5018
5131
|
).option(
|
|
5019
5132
|
"--description-file <path>",
|
|
5020
5133
|
'read the description from a UTF-8 file instead of --description; "-" reads standard input'
|
|
5021
|
-
).option("--
|
|
5134
|
+
).option("--target <branch>", "target branch (default: develop); refs/heads/ prefix accepted").option("--source <branch>", "source branch (default: the current branch); no local checkout needed").option("--draft", "open the pull request as a draft").option(
|
|
5135
|
+
"--work-item <id>",
|
|
5136
|
+
"link a work item to the pull request (positive integer; repeatable)",
|
|
5137
|
+
collectOption,
|
|
5138
|
+
[]
|
|
5139
|
+
).option("--label <label>", "add a label to the pull request (repeatable)", collectOption, []).option("--json", "output JSON; adds top-level id and url next to pullRequest").action(async (options) => {
|
|
5022
5140
|
validateOrgProjectPair(options);
|
|
5023
5141
|
const title = options.title?.trim();
|
|
5024
5142
|
if (!title) {
|
|
@@ -5029,45 +5147,71 @@ function createPrOpenCommand() {
|
|
|
5029
5147
|
if (description === null) {
|
|
5030
5148
|
return;
|
|
5031
5149
|
}
|
|
5150
|
+
const openOptions = resolveOpenOptions(options);
|
|
5151
|
+
if (openOptions === null) {
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
5032
5154
|
let context;
|
|
5033
5155
|
try {
|
|
5034
|
-
const
|
|
5035
|
-
|
|
5036
|
-
if (
|
|
5037
|
-
writeError(
|
|
5156
|
+
const openBranch = openOptions.source ?? getCurrentBranch();
|
|
5157
|
+
const target = openOptions.targetBranch;
|
|
5158
|
+
if (openBranch === target) {
|
|
5159
|
+
writeError(
|
|
5160
|
+
target === "develop" ? "Pull request creation requires a source branch other than develop." : `Pull request creation requires a source branch other than the target branch (${target}).`
|
|
5161
|
+
);
|
|
5038
5162
|
return;
|
|
5039
5163
|
}
|
|
5040
|
-
const
|
|
5164
|
+
const resolved = await resolvePrCommandContext(options, { requireBranch: false });
|
|
5165
|
+
context = resolved.context;
|
|
5041
5166
|
const result = await openPullRequest(
|
|
5042
5167
|
resolved.context,
|
|
5043
5168
|
resolved.repo,
|
|
5044
5169
|
resolved.pat,
|
|
5045
5170
|
openBranch,
|
|
5046
5171
|
title,
|
|
5047
|
-
description
|
|
5172
|
+
description,
|
|
5173
|
+
openOptions
|
|
5048
5174
|
);
|
|
5049
5175
|
if (options.json) {
|
|
5050
|
-
|
|
5176
|
+
const output = { ...result, id: result.pullRequest.id, url: result.pullRequest.url };
|
|
5177
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
5051
5178
|
`);
|
|
5179
|
+
reportWorkItemLinkFailures(result);
|
|
5052
5180
|
return;
|
|
5053
5181
|
}
|
|
5054
5182
|
if (result.created) {
|
|
5055
5183
|
process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
|
|
5056
5184
|
${result.pullRequest.url ?? "\u2014"}
|
|
5057
5185
|
`);
|
|
5186
|
+
reportWorkItemLinkFailures(result);
|
|
5058
5187
|
return;
|
|
5059
5188
|
}
|
|
5060
5189
|
process.stdout.write(
|
|
5061
|
-
`Active pull request already exists for ${
|
|
5190
|
+
`Active pull request already exists for ${openBranch} -> ${result.targetBranch}: #${result.pullRequest.id}
|
|
5062
5191
|
${result.pullRequest.url ?? "\u2014"}
|
|
5063
5192
|
`
|
|
5064
5193
|
);
|
|
5194
|
+
if (openOptions.isDraft || openOptions.labels?.length || openOptions.workItemIds?.length) {
|
|
5195
|
+
const hint = openOptions.workItemIds?.length ? " Use pr work-items link to link a work item." : "";
|
|
5196
|
+
process.stdout.write(
|
|
5197
|
+
`--draft, --label and --work-item were not applied; the existing pull request was left unchanged.${hint}
|
|
5198
|
+
`
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5065
5201
|
} catch (err) {
|
|
5066
|
-
handlePrOpenError(err, context);
|
|
5202
|
+
handlePrOpenError(err, context, openOptions.targetBranch);
|
|
5067
5203
|
}
|
|
5068
5204
|
});
|
|
5069
5205
|
return command;
|
|
5070
5206
|
}
|
|
5207
|
+
function reportWorkItemLinkFailures(result) {
|
|
5208
|
+
const failed = (result.workItems ?? []).filter((item) => !item.linked);
|
|
5209
|
+
if (failed.length === 0) return;
|
|
5210
|
+
const detail = failed.map((item) => `#${item.id} (${item.error ?? "unknown error"})`).join(", ");
|
|
5211
|
+
writeError(
|
|
5212
|
+
`Pull request #${result.pullRequest.id} was created but could not be linked to work item ${detail}. Retry with: azdo pr work-items link <id> --pr-number ${result.pullRequest.id}`
|
|
5213
|
+
);
|
|
5214
|
+
}
|
|
5071
5215
|
function handlePrUpdateError(err, context) {
|
|
5072
5216
|
const message = err instanceof Error ? err.message : "";
|
|
5073
5217
|
if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
|
|
@@ -7534,6 +7678,26 @@ function createPipelineCommand() {
|
|
|
7534
7678
|
|
|
7535
7679
|
// src/commands/comments.ts
|
|
7536
7680
|
import { Command as Command15 } from "commander";
|
|
7681
|
+
async function runCommentWrite(options, target, request, humanLine) {
|
|
7682
|
+
let context;
|
|
7683
|
+
try {
|
|
7684
|
+
context = resolveContext(options);
|
|
7685
|
+
const credential = await requireAuthCredential(context.org);
|
|
7686
|
+
const result = await request(context, credential);
|
|
7687
|
+
process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}
|
|
7688
|
+
` : `${humanLine(result)}
|
|
7689
|
+
`);
|
|
7690
|
+
} catch (err) {
|
|
7691
|
+
if (target.commentId === void 0) {
|
|
7692
|
+
handleCommandError(err, target.id, context, "write");
|
|
7693
|
+
} else {
|
|
7694
|
+
handleCommentWriteError(err, target.id, target.commentId, context);
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
function commentFormat(options) {
|
|
7699
|
+
return options.markdown === true ? "markdown" : "html";
|
|
7700
|
+
}
|
|
7537
7701
|
function writeError3(message) {
|
|
7538
7702
|
process.stderr.write(`Error: ${message}
|
|
7539
7703
|
`);
|
|
@@ -7588,22 +7752,74 @@ function createCommentsAddCommand() {
|
|
|
7588
7752
|
if (text.trim() === "") {
|
|
7589
7753
|
writeError3("Comment text must be a non-empty string.");
|
|
7590
7754
|
}
|
|
7591
|
-
|
|
7755
|
+
await runCommentWrite(
|
|
7756
|
+
options,
|
|
7757
|
+
{ id },
|
|
7758
|
+
(context, credential) => addWorkItemComment(context, id, credential, text, commentFormat(options)),
|
|
7759
|
+
(result) => `Added comment #${result.commentId} to work item #${result.workItemId}`
|
|
7760
|
+
);
|
|
7761
|
+
});
|
|
7762
|
+
return command;
|
|
7763
|
+
}
|
|
7764
|
+
function parseCommentId(value) {
|
|
7765
|
+
if (!/^\d+$/.test(value) || Number(value) < 1) {
|
|
7766
|
+
writeError3(`Invalid comment ID "${value}". Expected a positive integer.`);
|
|
7767
|
+
}
|
|
7768
|
+
return Number(value);
|
|
7769
|
+
}
|
|
7770
|
+
function resolveEditText(inline, file) {
|
|
7771
|
+
if (inline !== void 0 && file !== void 0) {
|
|
7772
|
+
writeError3("Cannot specify both inline text and --file.");
|
|
7773
|
+
}
|
|
7774
|
+
let body = inline;
|
|
7775
|
+
if (file !== void 0) {
|
|
7592
7776
|
try {
|
|
7593
|
-
|
|
7594
|
-
const credential = await requireAuthCredential(context.org);
|
|
7595
|
-
const format = options.markdown === true ? "markdown" : "html";
|
|
7596
|
-
const result = await addWorkItemComment(context, id, credential, text, format);
|
|
7597
|
-
if (options.json) {
|
|
7598
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
7599
|
-
`);
|
|
7600
|
-
return;
|
|
7601
|
-
}
|
|
7602
|
-
process.stdout.write(`Added comment #${result.commentId} to work item #${result.workItemId}
|
|
7603
|
-
`);
|
|
7777
|
+
body = readTextSource(file);
|
|
7604
7778
|
} catch (err) {
|
|
7605
|
-
|
|
7779
|
+
writeError3(err.message);
|
|
7606
7780
|
}
|
|
7781
|
+
}
|
|
7782
|
+
if (body === void 0 || body.trim() === "") {
|
|
7783
|
+
writeError3("Comment text must be a non-empty string. Pass the text inline or use --file <path>.");
|
|
7784
|
+
}
|
|
7785
|
+
return body;
|
|
7786
|
+
}
|
|
7787
|
+
function handleCommentWriteError(err, id, commentId, context) {
|
|
7788
|
+
if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
|
|
7789
|
+
writeError3(
|
|
7790
|
+
`Comment ${commentId} not found on work item ${id} in ${context?.org}/${context?.project}.`
|
|
7791
|
+
);
|
|
7792
|
+
}
|
|
7793
|
+
handleCommandError(err, id, context, "write");
|
|
7794
|
+
}
|
|
7795
|
+
function createCommentsEditCommand() {
|
|
7796
|
+
const command = new Command15("edit");
|
|
7797
|
+
command.description("Edit an existing comment on a work item").argument("<id>", "work item ID").argument("<commentId>", "comment ID").argument("[text]", "new comment text (or use --file)").option("--file <path>", 'read the new comment text from a file ("-" for stdin)').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, commentIdStr, text, options) => {
|
|
7798
|
+
validateOrgProjectPair(options);
|
|
7799
|
+
const id = parseWorkItemId(idStr);
|
|
7800
|
+
const commentId = parseCommentId(commentIdStr);
|
|
7801
|
+
const body = resolveEditText(text, options.file);
|
|
7802
|
+
await runCommentWrite(
|
|
7803
|
+
options,
|
|
7804
|
+
{ id, commentId },
|
|
7805
|
+
(context, credential) => updateWorkItemComment(context, id, commentId, credential, body, commentFormat(options)),
|
|
7806
|
+
(result) => `Updated comment #${result.commentId} on work item #${result.workItemId}`
|
|
7807
|
+
);
|
|
7808
|
+
});
|
|
7809
|
+
return command;
|
|
7810
|
+
}
|
|
7811
|
+
function createCommentsDeleteCommand() {
|
|
7812
|
+
const command = new Command15("delete");
|
|
7813
|
+
command.description("Delete a comment from a work item (no confirmation prompt)").argument("<id>", "work item ID").argument("<commentId>", "comment ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (idStr, commentIdStr, options) => {
|
|
7814
|
+
validateOrgProjectPair(options);
|
|
7815
|
+
const id = parseWorkItemId(idStr);
|
|
7816
|
+
const commentId = parseCommentId(commentIdStr);
|
|
7817
|
+
await runCommentWrite(
|
|
7818
|
+
options,
|
|
7819
|
+
{ id, commentId },
|
|
7820
|
+
(context, credential) => deleteWorkItemComment(context, id, commentId, credential),
|
|
7821
|
+
(result) => `Deleted comment #${result.commentId} from work item #${result.workItemId}`
|
|
7822
|
+
);
|
|
7607
7823
|
});
|
|
7608
7824
|
return command;
|
|
7609
7825
|
}
|
|
@@ -7612,6 +7828,8 @@ function createCommentsCommand() {
|
|
|
7612
7828
|
command.description("Manage Azure DevOps work item comments");
|
|
7613
7829
|
command.addCommand(createCommentsListCommand());
|
|
7614
7830
|
command.addCommand(createCommentsAddCommand());
|
|
7831
|
+
command.addCommand(createCommentsEditCommand());
|
|
7832
|
+
command.addCommand(createCommentsDeleteCommand());
|
|
7615
7833
|
return command;
|
|
7616
7834
|
}
|
|
7617
7835
|
|