Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: deno lint

- name: Type check
run: deno check --all
run: deno task check

- name: Run tests
run: deno task test
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- add issue commits command to print previous commits associated with an issue

## [1.4.0] - 2025-12-08

### Added
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ deno task install
the CLI works with both git and jj version control systems:

- **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).
- **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`.
- **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)"`

## commands

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

```bash
linear issue view # view issue details in terminal
linear issue view # view current issue details in terminal
linear issue view ABC-123
linear issue view 123
linear issue view -w # open issue in web browser
linear issue view -a # open issue in Linear.app
linear issue id # prints the issue id from current branch (e.g., "ENG-123")
Expand All @@ -119,6 +121,7 @@ linear issue comment list # list comments on current issue
linear issue comment add # add a comment to current issue
linear issue comment add -p <id> # reply to a specific comment
linear issue comment update <id> # update a comment
linear issue commits # show all commits for an issue (jj only)
```

### team commands
Expand Down
75 changes: 75 additions & 0 deletions src/commands/issue/issue-commits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Command } from "@cliffy/command"
import { isClientError, logClientError } from "../../utils/graphql.ts"
import { getIssueId, getIssueIdentifier } from "../../utils/linear.ts"
import { getNoIssueFoundMessage, getVcs } from "../../utils/vcs.ts"

export const commitsCommand = new Command()
.name("commits")
.description("Show all commits for a Linear issue (jj only)")
.arguments("[issueId:string]")
.action(async (_options, issueId) => {
const vcs = getVcs()

if (vcs !== "jj") {
console.error("✗ commits is only supported with jj-vcs")
Deno.exit(1)
}

const resolvedId = await getIssueIdentifier(issueId)
if (!resolvedId) {
console.error(getNoIssueFoundMessage())
Deno.exit(1)
}

// Verify the issue exists in Linear
let linearIssueId: string | undefined
try {
linearIssueId = await getIssueId(resolvedId)
} catch (error) {
if (isClientError(error)) {
logClientError(error)
Deno.exit(1)
}
throw error
}
if (!linearIssueId) {
console.error(`✗ issue not found: ${resolvedId}`)
Deno.exit(1)
}

// Build the revset to find all commits with this Linear issue
const revset = `description(regex:"(?m)^Linear-issue:.*${resolvedId}")`

// First check if any commits exist
const checkProcess = new Deno.Command("jj", {
args: ["log", "-r", revset, "-T", "commit_id", "--no-graph"],
stdout: "piped",
stderr: "piped",
})
const checkResult = await checkProcess.output()
const commitIds = new TextDecoder().decode(checkResult.stdout).trim()

if (!commitIds) {
console.error(`✗ no commits found for ${resolvedId}`)
Deno.exit(1)
}

// Show the commits with full details
const process = new Deno.Command("jj", {
args: [
"log",
"-r",
revset,
"-p",
"--git",
"--no-graph",
"-T",
"builtin_log_compact_full_description",
],
stdout: "inherit",
stderr: "inherit",
})

const { code } = await process.output()
Deno.exit(code)
})
4 changes: 2 additions & 2 deletions src/commands/issue/issue-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkStringify from "remark-stringify"
import { visit } from "unist-util-visit"
import type { Image } from "mdast"
import type { Image, Root } from "mdast"
import { shouldEnableHyperlinks } from "../../utils/hyperlink.ts"
import { createHyperlinkExtension } from "../../utils/charmd-hyperlink-extension.ts"

Expand Down Expand Up @@ -347,7 +347,7 @@ export async function replaceImageUrls(
): Promise<string> {
const processor = unified()
.use(remarkParse)
.use(() => (tree) => {
.use(() => (tree: Root) => {
visit(tree, "image", (node: Image) => {
const localPath = urlToPath.get(node.url)
if (localPath) {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/issue/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { commentCommand } from "./issue-comment.ts"
import { createCommand } from "./issue-create.ts"
import { deleteCommand } from "./issue-delete.ts"
import { describeCommand } from "./issue-describe.ts"
import { commitsCommand } from "./issue-commits.ts"
import { idCommand } from "./issue-id.ts"
import { listCommand } from "./issue-list.ts"
import { pullRequestCommand } from "./issue-pull-request.ts"
Expand All @@ -24,6 +25,7 @@ export const issueCommand = new Command()
.command("view", viewCommand)
.command("url", urlCommand)
.command("describe", describeCommand)
.command("commits", commitsCommand)
.command("pull-request", pullRequestCommand)
.command("delete", deleteCommand)
.command("create", createCommand)
Expand Down
35 changes: 34 additions & 1 deletion src/utils/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
import { GraphQLClient } from "graphql-request"
import { ClientError, GraphQLClient } from "graphql-request"
import { gray, setColorEnabled } from "@std/fmt/colors"
import { getOption } from "../config.ts"

export { ClientError }

/**
* Checks if an error is a GraphQL ClientError
*/
export function isClientError(error: unknown): error is ClientError {
return error instanceof ClientError
}

/**
* Logs a GraphQL ClientError formatted for display to the user
*/
export function logClientError(error: ClientError): void {
const userMessage = error.response?.errors?.[0]?.extensions
?.userPresentableMessage as
| string
| undefined
const message = userMessage?.toLowerCase() ?? error.message

console.error(`✗ ${message}\n`)

const rawQuery = error.request?.query
const query = typeof rawQuery === "string" ? rawQuery.trim() : rawQuery
const vars = JSON.stringify(error.request?.variables, null, 2)

setColorEnabled(Deno.stderr.isTerminal())

console.error(gray(String(query)))
console.error("")
console.error(gray(vars))
}

export function getGraphQLClient(): GraphQLClient {
const apiKey = getOption("api_key")
if (!apiKey) {
Expand Down
19 changes: 19 additions & 0 deletions test/commands/issue/__snapshots__/issue-commits.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const snapshot = {};

snapshot[`Issue Commits Command - Help Text 1`] = `
stdout:
"
Usage: commits [issueId]

Description:

Show all commits for a Linear issue (jj only)

Options:

-h, --help - Show this help.

"
stderr:
""
`;
24 changes: 24 additions & 0 deletions test/commands/issue/issue-commits.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { snapshotTest } from "@cliffy/testing"
import { commitsCommand } from "../../../src/commands/issue/issue-commits.ts"

// Common Deno args for permissions
const denoArgs = [
"--allow-env=GITHUB_*,GH_*,LINEAR_*,NODE_ENV,EDITOR,PAGER,SNAPSHOT_TEST_NAME,CLIFFY_SNAPSHOT_FAKE_TIME,NO_COLOR,TMPDIR,TMP,TEMP",
"--allow-read",
"--allow-write",
"--allow-run",
"--allow-net",
"--quiet",
]

// Test help output
await snapshotTest({
name: "Issue Commits Command - Help Text",
meta: import.meta,
colors: false,
args: ["--help"],
denoArgs,
async fn() {
await commitsCommand.parse()
},
})