Files
deepseek-harness/packages/experimental/forge-github/src/compare.ts
T
Yichen Jiang e07b170d77 fix(forge): address review of pinned diffs and thread counting
- forge-github: repeat a compare read without --allow-escape-sequences when
  a gh older than 2.97 rejects the option; such a gh prints escape sequences
  unchanged, so every version reads every diff (checked with gh 2.96.0 and
  2.100.0 on a pull request whose patch holds an ESC byte).
- client-ui-forge: a stale refusal now re-runs the read effect, so a status
  that arrived during the read, including a failed poll that keeps the
  earlier head, starts the next read at once instead of leaving the changes
  view loading.
- forge: ctx.forge.diff polls at once only for the first stale read at a head
  commit; later stale reads at it wait for the regular poll, which bounds the
  read-poll loop while GitHub's answers briefly disagree.
- forge-github: review thread pages share the byte budget of a complete
  check read.
- client-ui-forge: a truncated count shows its "+" even when it is zero.
- READMEs: document the batch deadline and rewritten-base limitations.
2026-11-06 18:17:04 +08:00

44 lines
2.5 KiB
TypeScript

/** Reads GitHub's patch between two commits through the REST compare endpoint. */
import type { ForgeFailure, ForgeRepository } from '@deepseek-ai/dsh-experimental-forge'
import { isCommit } from './urls.ts'
/** Runs one reading `gh` command and maps a refusal to a failure, as the provider's reads do. */
export type GhTextReader = (args: readonly string[], signal: AbortSignal) => Promise<string | ForgeFailure>
/** `gh` option that prints a response containing terminal escape sequences; `gh` 2.97 added it. */
const ALLOW_ESCAPES = '--allow-escape-sequences'
/** What a `gh` older than 2.97 reports for {@link ALLOW_ESCAPES}, before it sends any request. */
const UNKNOWN_ALLOW_ESCAPES = `unknown flag: ${ALLOW_ESCAPES}`
/**
* Read the unified patch from the merge base of `base` and `head` to `head`, as
* GitHub computes the diff of a pull request with that base and head commit.
* Both ends are commit object names, so the answer never follows a moving
* branch. The patch keeps terminal escape sequences that file contents hold:
* `gh` 2.97 and newer refuses to print them without `--allow-escape-sequences`,
* and an older `gh`, which rejects that option, prints them unchanged, so a
* rejection of the option alone repeats the read without it.
* GitHub returns the patch however many files it changes; `read` bounds its size.
* @param read - runs `gh` with the provider's output limit and failure mapping.
* @param repository - repository that holds both commits, with an owner and name already checked for inlining.
* @param base - commit whose merge base with `head` starts the patch.
* @param head - commit the patch ends at.
* @param signal - cancellation of the read.
* @returns the patch with `a/` and `b/` path prefixes, or why GitHub did not provide it.
* @throws when `base` or `head` is not a full commit object name.
*/
export async function readComparison(
read: GhTextReader, repository: ForgeRepository, base: string, head: string, signal: AbortSignal,
): Promise<string | ForgeFailure> {
for (const commit of [base, head]) {
if (!isCommit(commit)) throw new Error(`forge-github: invalid commit ${JSON.stringify(commit)}`)
}
const { host, owner, name } = repository
const request = ['-H', 'Accept: application/vnd.github.diff', `repos/${owner}/${name}/compare/${base}...${head}`]
const patch = await read(['api', '--hostname', host, ALLOW_ESCAPES, ...request], signal)
if (typeof patch === 'string' || !patch.message.startsWith(UNKNOWN_ALLOW_ESCAPES)) return patch
return await read(['api', '--hostname', host, ...request], signal)
}