Skip to content

Commit b08089a

Browse files
author
Joan-Angelo Enrile
committed
feat: implement comment thread functionality for pull requests
1 parent 446d0fe commit b08089a

8 files changed

Lines changed: 91 additions & 76 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ ado pr view <pr>
8585
ado pr view <pr> --comments
8686
ado pr diff <pr>
8787
ado pr diff <pr> --name-only
88-
ado pr comment <pr> --body "Looks good"
88+
ado pr thread create <pr> --body "Looks good to me"
89+
ado pr thread create <pr> --file src/api/client.ts --line 42 --body "This could be simplified"
90+
ado pr thread create <pr> --file src/api/client.ts --line 42-55 --body "Consider extracting this block"
91+
ado pr thread reply <pr> <thread-id> --body "Fixed in latest commit"
92+
ado pr thread status <pr> <thread-id> --resolve
8993
ado pr create --title "My PR" --draft
9094
ado pr review <pr> --approve
9195
ado pr review <pr> --reject --comment "Needs changes"

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- [x] `ado issue view <id>` — work item detail view with optional comments
1111
- [x] `ado pr list` — list PRs with state/author/repo filters
1212
- [x] `ado pr view <pr>` — PR detail with optional comment threads
13-
- [x] `ado pr comment <pr>`add comment (--body or $EDITOR)
13+
- [x] `ado pr thread create <pr>`create a new comment thread (--body or $EDITOR; optionally anchored to --file + --line range)
1414
- [x] `ado pr diff <pr>` — real unified diff with `--color`, `--exclude`, `--name-only`, `--patch`, `--web` flags matching `gh pr diff`
1515
- [x] OAuth browser login as default auth (MSAL `acquireTokenInteractive`); device code flow for headless/CI
1616
- [x] DEP0169 deprecation warning suppression (upstream: azure-devops-node-api#664, fix: PR#662)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ado-cli",
3-
"version": "1.1.1",
3+
"version": "1.2.0",
44
"description": "An Azure DevOps CLI, mimicking the known and loved Github CLI syntax.",
55
"main": "./dist/index.js",
66
"bin": {

plugins/azure-devops/skills/azure-devops/reference.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,16 @@ Generated from source at `src/commands/**/*.ts`. Update this file whenever a com
143143
| `--json [fields]` || Output as JSON ||
144144
| `--web` | `-w` | Open in browser ||
145145

146-
## ado pr comment \<id\>
146+
## ado pr thread create \<pr-number\>
147+
148+
Create a new comment thread on a pull request. Optionally anchor the thread to a specific file and line range in the diff (targets the right/head side).
147149

148150
| Flag | Short | Description | Default |
149151
|------|-------|-------------|---------|
150-
| `--body <text>` | `-b` | Comment body ||
151-
| `--editor` || Open `$EDITOR` to write the comment ||
152+
| `--body <text>` | `-b` | Thread body ||
153+
| `--editor` || Open `$EDITOR` to write the thread body ||
154+
| `--file <path>` || File path to anchor the thread (relative to repo root) ||
155+
| `--line <n[-n]>` || Line number or range, e.g. `42` or `42-55`; requires `--file` ||
152156
| `--repo <repo>` | `-r` | Repository name ||
153157
| `--project <project>` | `-p` | Azure DevOps project ||
154158
| `--org <url>` || Organization URL ||

src/api/pullRequests.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,20 +218,28 @@ export async function getPullRequest(
218218
}
219219
}
220220

221-
export async function addPrComment(
221+
export async function createPrThread(
222222
connection: azdev.WebApi,
223223
project: string,
224224
repoName: string,
225225
prId: number,
226-
body: string
226+
body: string,
227+
fileContext?: { file: string; lineStart: number; lineEnd: number }
227228
): Promise<void> {
228229
const gitApi = await connection.getGitApi();
229230

231+
const threadContext = fileContext ? {
232+
filePath: fileContext.file,
233+
rightFileStart: { line: fileContext.lineStart, offset: 1 },
234+
rightFileEnd: { line: fileContext.lineEnd, offset: 1 },
235+
} : undefined;
236+
230237
try {
231238
await gitApi.createThread(
232239
{
233240
comments: [{ content: body, commentType: 1 }],
234241
status: 1, // Active
242+
threadContext,
235243
},
236244
repoName,
237245
prId,

src/commands/pr/comment.ts

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/commands/pr/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Command } from 'commander';
22
import { registerPrList } from './list.js';
33
import { registerPrView } from './view.js';
4-
import { registerPrComment } from './comment.js';
54
import { registerPrDiff } from './diff.js';
65
import { registerPrCreate } from './create.js';
76
import { registerPrReview } from './review.js';
@@ -11,7 +10,6 @@ export function registerPrCommands(program: Command): void {
1110
const pr = program.command('pr').description('Manage Azure DevOps pull requests');
1211
registerPrList(pr);
1312
registerPrView(pr);
14-
registerPrComment(pr);
1513
registerPrDiff(pr);
1614
registerPrCreate(pr);
1715
registerPrReview(pr);

src/commands/pr/thread.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as os from 'os';
44
import * as path from 'path';
55
import * as child_process from 'child_process';
66
import { getWebApi } from '../../api/client.js';
7-
import { replyToThread, setThreadStatus, resolveRepo } from '../../api/pullRequests.js';
7+
import { createPrThread, replyToThread, setThreadStatus, resolveRepo } from '../../api/pullRequests.js';
88
import { getConfig } from '../../config/index.js';
99
import { AdoError } from '../../errors/index.js';
1010

@@ -24,6 +24,60 @@ function openEditor(initial = ''): string {
2424
return content;
2525
}
2626

27+
async function prThreadCreateHandler(
28+
prId: string,
29+
options: { body?: string; editor?: boolean; file?: string; line?: string; repo?: string; project?: string; org?: string }
30+
): Promise<void> {
31+
const numId = parseInt(prId, 10);
32+
if (isNaN(numId)) {
33+
process.stderr.write(`ado: invalid PR number: ${prId}\n`);
34+
process.exit(1);
35+
}
36+
37+
if (options.line && !options.file) {
38+
process.stderr.write('ado: --line requires --file\n');
39+
process.exit(1);
40+
}
41+
42+
let body = options.body;
43+
if (!body || options.editor) {
44+
body = openEditor(body ?? '');
45+
}
46+
if (!body) {
47+
throw new AdoError('Thread body cannot be empty.');
48+
}
49+
50+
let fileContext: { file: string; lineStart: number; lineEnd: number } | undefined;
51+
if (options.file) {
52+
let lineStart = 1;
53+
let lineEnd = 1;
54+
if (options.line) {
55+
const match = options.line.match(/^(\d+)(?:-(\d+))?$/);
56+
if (!match) {
57+
process.stderr.write(`ado: invalid --line value: ${options.line} (use N or N-M)\n`);
58+
process.exit(1);
59+
}
60+
lineStart = parseInt(match[1], 10);
61+
lineEnd = match[2] !== undefined ? parseInt(match[2], 10) : lineStart;
62+
if (lineEnd < lineStart) {
63+
process.stderr.write('ado: --line end must be >= start\n');
64+
process.exit(1);
65+
}
66+
}
67+
fileContext = { file: options.file, lineStart, lineEnd };
68+
}
69+
70+
const config = getConfig({ orgUrl: options.org, project: options.project });
71+
const connection = await getWebApi(config.orgUrl);
72+
const repoName = await resolveRepo(connection, config.project, options.repo);
73+
74+
await createPrThread(connection, config.project, repoName, numId, body, fileContext);
75+
const location = fileContext
76+
? ` on ${fileContext.file}:${fileContext.lineStart}${fileContext.lineEnd !== fileContext.lineStart ? `-${fileContext.lineEnd}` : ''}`
77+
: '';
78+
process.stdout.write(`Created thread on PR #${numId}${location}\n`);
79+
}
80+
2781
async function prThreadReplyHandler(
2882
prId: string,
2983
threadId: string,
@@ -116,6 +170,18 @@ export function registerPrThread(prCmd: Command): void {
116170
.command('thread')
117171
.description('Manage pull request comment threads');
118172

173+
thread
174+
.command('create <pr-number>')
175+
.description('Create a new comment thread on a pull request')
176+
.option('-b, --body <text>', 'Thread body')
177+
.option('--editor', 'Open $EDITOR to write the thread body')
178+
.option('--file <path>', 'File path to anchor the thread to (relative to repo root)')
179+
.option('--line <n[-n]>', 'Line number or range (e.g. 42 or 42-55); requires --file')
180+
.option('-r, --repo <repo>', 'Repository name')
181+
.option('-p, --project <project>', 'Azure DevOps project (overrides config)')
182+
.option('--org <url>', 'Azure DevOps organization URL (overrides config)')
183+
.action(prThreadCreateHandler);
184+
119185
thread
120186
.command('reply <pr-number> <thread-id>')
121187
.description('Reply to an existing comment thread')

0 commit comments

Comments
 (0)