add opencode review plugin (port from pi-coding-agent extension)
This commit is contained in:
237
.opencode/plugins/review.ts
Normal file
237
.opencode/plugins/review.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { type Plugin, tool } from "@opencode-ai/plugin"
|
||||
|
||||
function normalizeRemoteUrl(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^git@github\.com:/, "https://github.com/")
|
||||
.replace(/^ssh:\/\/git@github\.com\//, "https://github.com/")
|
||||
.replace(/\.git$/, "")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function sanitizeRemoteName(value: string): string {
|
||||
const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")
|
||||
return sanitized || "gh-pr"
|
||||
}
|
||||
|
||||
export const ReviewPlugin: Plugin = async ({ $ }) => {
|
||||
return {
|
||||
tool: {
|
||||
review_materialize_pr: tool({
|
||||
description:
|
||||
"Materialize a GitHub pull request locally using jj for code review. " +
|
||||
"Fetches the PR branch, creates a new jj change on top of it, and returns " +
|
||||
"metadata needed for the review. Handles cross-repository (forked) PRs. " +
|
||||
"Call this before reviewing a PR to set up the local state.",
|
||||
args: {
|
||||
prNumber: tool.schema
|
||||
.number()
|
||||
.describe("The PR number to materialize (e.g. 123)"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const prNumber = args.prNumber
|
||||
|
||||
// Check for pending working-copy changes
|
||||
const statusResult =
|
||||
await $`jj diff --summary 2>/dev/null`.nothrow().quiet()
|
||||
if (
|
||||
statusResult.exitCode === 0 &&
|
||||
statusResult.stdout.toString().trim().length > 0
|
||||
) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error:
|
||||
"Cannot materialize PR: you have local jj changes. Please snapshot or discard them first.",
|
||||
})
|
||||
}
|
||||
|
||||
// Save current position for later restoration
|
||||
const currentChangeResult =
|
||||
await $`jj log -r @ --no-graph -T 'change_id.shortest(8)'`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
const savedChangeId = currentChangeResult.stdout.toString().trim()
|
||||
|
||||
// Get PR info from GitHub CLI
|
||||
const prInfoResult =
|
||||
await $`gh pr view ${prNumber} --json baseRefName,title,headRefName,isCrossRepository,headRepository,headRepositoryOwner`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
if (prInfoResult.exitCode !== 0) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: `Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`,
|
||||
})
|
||||
}
|
||||
|
||||
let prInfo: {
|
||||
baseRefName: string
|
||||
title: string
|
||||
headRefName: string
|
||||
isCrossRepository: boolean
|
||||
headRepository?: { name: string; url: string }
|
||||
headRepositoryOwner?: { login: string }
|
||||
}
|
||||
try {
|
||||
prInfo = JSON.parse(prInfoResult.stdout.toString())
|
||||
} catch {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "Failed to parse PR info from gh CLI",
|
||||
})
|
||||
}
|
||||
|
||||
// Determine the remote to use
|
||||
const remotesResult =
|
||||
await $`jj git remote list`.nothrow().quiet()
|
||||
const remotes = remotesResult.stdout
|
||||
.toString()
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line: string) => {
|
||||
const [name, ...urlParts] = line.split(/\s+/)
|
||||
return { name, url: urlParts.join(" ") }
|
||||
})
|
||||
.filter(
|
||||
(r: { name: string; url: string }) => r.name && r.url,
|
||||
)
|
||||
|
||||
const defaultRemote =
|
||||
remotes.find(
|
||||
(r: { name: string; url: string }) =>
|
||||
r.name === "origin",
|
||||
) ?? remotes[0]
|
||||
if (!defaultRemote) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "No jj remotes are configured for this repository",
|
||||
})
|
||||
}
|
||||
|
||||
let remoteName = defaultRemote.name
|
||||
let addedTemporaryRemote = false
|
||||
|
||||
if (prInfo.isCrossRepository) {
|
||||
const repoSlug =
|
||||
prInfo.headRepositoryOwner?.login &&
|
||||
prInfo.headRepository?.name
|
||||
? `${prInfo.headRepositoryOwner.login}/${prInfo.headRepository.name}`.toLowerCase()
|
||||
: undefined
|
||||
const forkUrl = prInfo.headRepository?.url
|
||||
|
||||
// Check if we already have a remote for this fork
|
||||
const existingRemote = remotes.find(
|
||||
(r: { name: string; url: string }) => {
|
||||
if (
|
||||
forkUrl &&
|
||||
normalizeRemoteUrl(r.url) ===
|
||||
normalizeRemoteUrl(forkUrl)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return repoSlug
|
||||
? normalizeRemoteUrl(r.url).includes(
|
||||
`github.com/${repoSlug}`,
|
||||
)
|
||||
: false
|
||||
},
|
||||
)
|
||||
|
||||
if (existingRemote) {
|
||||
remoteName = existingRemote.name
|
||||
} else if (forkUrl) {
|
||||
const remoteBaseName = sanitizeRemoteName(
|
||||
`gh-pr-${prInfo.headRepositoryOwner?.login ?? "remote"}-${prInfo.headRepository?.name ?? prNumber}`,
|
||||
)
|
||||
const existingNames = new Set(
|
||||
remotes.map(
|
||||
(r: { name: string; url: string }) =>
|
||||
r.name,
|
||||
),
|
||||
)
|
||||
remoteName = remoteBaseName
|
||||
let suffix = 2
|
||||
while (existingNames.has(remoteName)) {
|
||||
remoteName = `${remoteBaseName}-${suffix}`
|
||||
suffix += 1
|
||||
}
|
||||
const addResult =
|
||||
await $`jj git remote add ${remoteName} ${forkUrl}`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
if (addResult.exitCode !== 0) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error:
|
||||
addResult.stderr.toString() ||
|
||||
"Failed to add PR remote",
|
||||
})
|
||||
}
|
||||
addedTemporaryRemote = true
|
||||
} else {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "PR head repository URL is unavailable",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the PR branch
|
||||
const fetchResult =
|
||||
await $`jj git fetch --remote ${remoteName} --branch ${prInfo.headRefName}`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
if (fetchResult.exitCode !== 0) {
|
||||
if (addedTemporaryRemote) {
|
||||
await $`jj git remote remove ${remoteName}`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
}
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error:
|
||||
fetchResult.stderr.toString() ||
|
||||
"Failed to fetch PR branch",
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new change on top of the PR branch
|
||||
const bookmarkRevset = `remote_bookmarks(exact:"${prInfo.headRefName}", exact:"${remoteName}")`
|
||||
const editResult =
|
||||
await $`jj new ${bookmarkRevset}`.nothrow().quiet()
|
||||
if (editResult.exitCode !== 0) {
|
||||
if (addedTemporaryRemote) {
|
||||
await $`jj git remote remove ${remoteName}`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
}
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error:
|
||||
editResult.stderr.toString() ||
|
||||
"Failed to create change on PR branch",
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up temporary remote
|
||||
if (addedTemporaryRemote) {
|
||||
await $`jj git remote remove ${remoteName}`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
success: true,
|
||||
prNumber,
|
||||
title: prInfo.title,
|
||||
baseBookmark: prInfo.baseRefName,
|
||||
headBookmark: prInfo.headRefName,
|
||||
remote: remoteName,
|
||||
savedChangeId,
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user