anthropics / anthropics/claude-code-action

fetchGitHubData spawns one blocking git hash-object process per changed file

Open
#1,666 0 comments 0 reactions 0 assignees View on GitHub
p3 performance
Dominant language
TypeScript
Stars
8.9k
Forks
2.1k
PR merge metrics
PR metrics pending

Description

## Summary

[`fetchGitHubData`](https://github.com/anthropics/claude-code-action/blob/d721746d683d812e669ce117cebe55a85fbd9c3e/src/github/data/fetcher.ts#L479-L509) computes a blob SHA for every changed file by spawning a separate synchronous `git hash-object` process per file. On a large PR this is hundreds of process spawns, executed with `execFileSync` inside an `async` function, blocking the event loop for the duration.

## Current code

`src/github/data/fetcher.ts:479-509`:

```ts
if (isPR && changedFiles.length > 0) {
changedFilesWithSHA = changedFiles.map((file) => {
if (file.changeType === "DELETED") {
return { ...file, sha: "deleted" };
}
try {
// Use git hash-object to compute the SHA for the current file content
const sha = execFileSync("git", ["hash-object", file.path], {
encoding: "utf-8",
}).trim();
return { ...file, sha };
} catch (error) {
console.warn(`Failed to compute SHA for ${file.path}:`, error);
return { ...file, sha: "unknown" };
}
});
}
```

One `fork`/`exec` per non-deleted changed file. The cost scales linearly with PR size and is paid on every tag-mode invocation against a PR, before Claude starts.

## Impact

`git hash-object` itself is fast; the dominant cost is process creation. On GitHub-hosted runners a spawn is on the order of single-digit milliseconds, so a 400-file PR spends roughly a second here — and self-hosted or container runners with slower `fork` are worse. The work is entirely serial and entirely avoidable.

It also blocks. `fetchGitHubData` is `async` and this loop is synchronous, so nothing else on the event loop progresses while it runs.

## Suggested fix

`git hash-object` accepts `--stdin-paths`, reading newline-separated paths and emitting one SHA per line in input order. The whole loop collapses to a single spawn:

```console
$ printf 'src/a.ts\nsrc/b.ts\n' | git hash-object --stdin-paths
b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0
a1b2c3d4e5f6...
```

Two behaviours must be preserved, and are the reason this is worth doing carefully rather than as a naive swap:

1. **`DELETED` files must stay excluded** — they have no working-tree content, so they must not enter the batch and must keep `sha: "deleted"`. The batched output has to be zipped back onto the filtered subset, not the full list.
2. **Per-file failure isolation** — today a file that cannot be hashed yields `sha: "unknown"` and the rest still succeed. `--stdin-paths` aborts the whole batch on the first unreadable path, so a fallback is needed: on batch failure, retry per file (current behaviour) so one bad path cannot degrade every SHA to `"unknown"`.

There are existing open issues about `git hash-object` *failing* in this code path — #979 (newly added files hashed before checkout), #730 (PRs that delete files), #239 (paths containing `$`). This issue is specifically about the **cost** of the per-file spawn, not those failures, but the batching rewrite touches the same lines and the fallback described above would need to keep their behaviour intact. Worth coordinating if any of those are being worked on.

## Minor, same lines

`file.path` is passed as a bare argument with no `--` separator. A repository file whose name begins with `-` would be parsed by `git` as an option rather than a path. `execFileSync` means there is no shell involved, so this is not an injection concern — just an argument-parsing edge case that a `--` would close, and which is free to add while touching this call.

## Context

`src/github/data/fetcher.ts` is a core module — `test/data-fetcher.test.ts` is the largest test file in the repo (94 test blocks), so there is good existing scaffolding to extend for the batching logic and the fallback path.

I'm happy to open a PR with the batched implementation plus tests covering the deleted-file exclusion, the ordering guarantee, and the per-file fallback.

## Environment

- Repository at `d721746d683d812e669ce117cebe55a85fbd9c3e` (`main`)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.