Skip to content

Commit 2822ff2

Browse files
committed
Add issue commits command
Implement 'linear issue commits' to show all commits associated with a Linear issue by searching for commits with the Linear-issue trailer. - Add issue-commits.ts command that uses jj revsets to find matching commits - Verify issue exists in Linear before displaying commits - Display error if no commits found for the issue - Add logClientError helper to format GraphQL errors with syntax highlighting - Errors display query and variables in gray text that respects TTY detection - Add snapshot test for help text, manual testing needed for main flow Claude-session-id: 5a3c2426-d8b9-4e47-b694-05ae5e889207 Claude-session-id: b3d1bee0-92ed-4873-ae82-7a09c45d95cb Claude-session-id: 519d0b6c-1a76-4755-9417-d5c80d519450 Claude-session-id: d48d5f7a-1525-43db-a3fb-4c361bf8fddd
1 parent 60ed8d7 commit 2822ff2

File tree

9 files changed

+166
-6
lines changed

9 files changed

+166
-6
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
run: deno lint
2525

2626
- name: Type check
27-
run: deno check --all
27+
run: deno task check
2828

2929
- name: Run tests
3030
run: deno task test

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- add issue commits command to print previous commits associated with an issue
8+
59
## [1.4.0] - 2025-12-08
610

711
### Added

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ deno task install
8686
the CLI works with both git and jj version control systems:
8787

8888
- **git**: works best when your branches include Linear issue IDs (e.g. `eng-123-my-feature`). use `linear issue start` or linear UI's 'copy git branch name' button and [related automations](https://linear.app/docs/account-preferences#git-related-automations).
89-
- **jj**: detects issues from `Linear-issue` trailers in your commit descriptions. use `linear issue start` to automatically add the trailer, or add it manually with `jj describe`.
89+
- **jj**: detects issues from `Linear-issue` trailers in your commit descriptions. use `linear issue start` to automatically add the trailer, or add it manually with `jj describe`, e.g. `jj describe "$(linear issue describe ABC-123)"`
9090

9191
## commands
9292

@@ -100,7 +100,9 @@ the current issue is determined by:
100100
note that [Linear's GitHub integration](https://linear.app/docs/github#branch-format) will suggest git branch names.
101101

102102
```bash
103-
linear issue view # view issue details in terminal
103+
linear issue view # view current issue details in terminal
104+
linear issue view ABC-123
105+
linear issue view 123
104106
linear issue view -w # open issue in web browser
105107
linear issue view -a # open issue in Linear.app
106108
linear issue id # prints the issue id from current branch (e.g., "ENG-123")
@@ -119,6 +121,7 @@ linear issue comment list # list comments on current issue
119121
linear issue comment add # add a comment to current issue
120122
linear issue comment add -p <id> # reply to a specific comment
121123
linear issue comment update <id> # update a comment
124+
linear issue commits # show all commits for an issue (jj only)
122125
```
123126

124127
### team commands
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Command } from "@cliffy/command"
2+
import { isClientError, logClientError } from "../../utils/graphql.ts"
3+
import { getIssueId, getIssueIdentifier } from "../../utils/linear.ts"
4+
import { getNoIssueFoundMessage, getVcs } from "../../utils/vcs.ts"
5+
6+
export const commitsCommand = new Command()
7+
.name("commits")
8+
.description("Show all commits for a Linear issue (jj only)")
9+
.arguments("[issueId:string]")
10+
.action(async (_options, issueId) => {
11+
const vcs = getVcs()
12+
13+
if (vcs !== "jj") {
14+
console.error("✗ commits is only supported with jj-vcs")
15+
Deno.exit(1)
16+
}
17+
18+
const resolvedId = await getIssueIdentifier(issueId)
19+
if (!resolvedId) {
20+
console.error(getNoIssueFoundMessage())
21+
Deno.exit(1)
22+
}
23+
24+
// Verify the issue exists in Linear
25+
let linearIssueId: string | undefined
26+
try {
27+
linearIssueId = await getIssueId(resolvedId)
28+
} catch (error) {
29+
if (isClientError(error)) {
30+
logClientError(error)
31+
Deno.exit(1)
32+
}
33+
throw error
34+
}
35+
if (!linearIssueId) {
36+
console.error(`✗ issue not found: ${resolvedId}`)
37+
Deno.exit(1)
38+
}
39+
40+
// Build the revset to find all commits with this Linear issue
41+
const revset = `description(regex:"(?m)^Linear-issue:.*${resolvedId}")`
42+
43+
// First check if any commits exist
44+
const checkProcess = new Deno.Command("jj", {
45+
args: ["log", "-r", revset, "-T", "commit_id", "--no-graph"],
46+
stdout: "piped",
47+
stderr: "piped",
48+
})
49+
const checkResult = await checkProcess.output()
50+
const commitIds = new TextDecoder().decode(checkResult.stdout).trim()
51+
52+
if (!commitIds) {
53+
console.error(`✗ no commits found for ${resolvedId}`)
54+
Deno.exit(1)
55+
}
56+
57+
// Show the commits with full details
58+
const process = new Deno.Command("jj", {
59+
args: [
60+
"log",
61+
"-r",
62+
revset,
63+
"-p",
64+
"--git",
65+
"--no-graph",
66+
"-T",
67+
"builtin_log_compact_full_description",
68+
],
69+
stdout: "inherit",
70+
stderr: "inherit",
71+
})
72+
73+
const { code } = await process.output()
74+
Deno.exit(code)
75+
})

src/commands/issue/issue-view.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { unified } from "unified"
1616
import remarkParse from "remark-parse"
1717
import remarkStringify from "remark-stringify"
1818
import { visit } from "unist-util-visit"
19-
import type { Image } from "mdast"
19+
import type { Image, Root } from "mdast"
2020
import { shouldEnableHyperlinks } from "../../utils/hyperlink.ts"
2121
import { createHyperlinkExtension } from "../../utils/charmd-hyperlink-extension.ts"
2222

@@ -347,7 +347,7 @@ export async function replaceImageUrls(
347347
): Promise<string> {
348348
const processor = unified()
349349
.use(remarkParse)
350-
.use(() => (tree) => {
350+
.use(() => (tree: Root) => {
351351
visit(tree, "image", (node: Image) => {
352352
const localPath = urlToPath.get(node.url)
353353
if (localPath) {

src/commands/issue/issue.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { commentCommand } from "./issue-comment.ts"
33
import { createCommand } from "./issue-create.ts"
44
import { deleteCommand } from "./issue-delete.ts"
55
import { describeCommand } from "./issue-describe.ts"
6+
import { commitsCommand } from "./issue-commits.ts"
67
import { idCommand } from "./issue-id.ts"
78
import { listCommand } from "./issue-list.ts"
89
import { pullRequestCommand } from "./issue-pull-request.ts"
@@ -24,6 +25,7 @@ export const issueCommand = new Command()
2425
.command("view", viewCommand)
2526
.command("url", urlCommand)
2627
.command("describe", describeCommand)
28+
.command("commits", commitsCommand)
2729
.command("pull-request", pullRequestCommand)
2830
.command("delete", deleteCommand)
2931
.command("create", createCommand)

src/utils/graphql.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
1-
import { GraphQLClient } from "graphql-request"
1+
import { ClientError, GraphQLClient } from "graphql-request"
2+
import { gray, setColorEnabled } from "@std/fmt/colors"
23
import { getOption } from "../config.ts"
34

5+
export { ClientError }
6+
7+
/**
8+
* Checks if an error is a GraphQL ClientError
9+
*/
10+
export function isClientError(error: unknown): error is ClientError {
11+
return error instanceof ClientError
12+
}
13+
14+
/**
15+
* Logs a GraphQL ClientError formatted for display to the user
16+
*/
17+
export function logClientError(error: ClientError): void {
18+
const userMessage = error.response?.errors?.[0]?.extensions
19+
?.userPresentableMessage as
20+
| string
21+
| undefined
22+
const message = userMessage?.toLowerCase() ?? error.message
23+
24+
console.error(`✗ ${message}\n`)
25+
26+
const rawQuery = error.request?.query
27+
const query = typeof rawQuery === "string" ? rawQuery.trim() : rawQuery
28+
const vars = JSON.stringify(error.request?.variables, null, 2)
29+
30+
setColorEnabled(Deno.stderr.isTerminal())
31+
32+
console.error(gray(String(query)))
33+
console.error("")
34+
console.error(gray(vars))
35+
}
36+
437
export function getGraphQLClient(): GraphQLClient {
538
const apiKey = getOption("api_key")
639
if (!apiKey) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const snapshot = {};
2+
3+
snapshot[`Issue Commits Command - Help Text 1`] = `
4+
stdout:
5+
"
6+
Usage: commits [issueId]
7+
8+
Description:
9+
10+
Show all commits for a Linear issue (jj only)
11+
12+
Options:
13+
14+
-h, --help - Show this help.
15+
16+
"
17+
stderr:
18+
""
19+
`;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { snapshotTest } from "@cliffy/testing"
2+
import { commitsCommand } from "../../../src/commands/issue/issue-commits.ts"
3+
4+
// Common Deno args for permissions
5+
const denoArgs = [
6+
"--allow-env=GITHUB_*,GH_*,LINEAR_*,NODE_ENV,EDITOR,PAGER,SNAPSHOT_TEST_NAME,CLIFFY_SNAPSHOT_FAKE_TIME,NO_COLOR,TMPDIR,TMP,TEMP",
7+
"--allow-read",
8+
"--allow-write",
9+
"--allow-run",
10+
"--allow-net",
11+
"--quiet",
12+
]
13+
14+
// Test help output
15+
await snapshotTest({
16+
name: "Issue Commits Command - Help Text",
17+
meta: import.meta,
18+
colors: false,
19+
args: ["--help"],
20+
denoArgs,
21+
async fn() {
22+
await commitsCommand.parse()
23+
},
24+
})

0 commit comments

Comments
 (0)