@nanonets/graft 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/app/brain-build.d.ts +66 -0
- package/dist/app/brain-build.d.ts.map +1 -0
- package/dist/app/brain-build.js +169 -0
- package/dist/app/brain-build.js.map +1 -0
- package/dist/app/checkout.d.ts +53 -3
- package/dist/app/checkout.d.ts.map +1 -1
- package/dist/app/checkout.js +195 -27
- package/dist/app/checkout.js.map +1 -1
- package/dist/app/events.d.ts +7 -0
- package/dist/app/events.d.ts.map +1 -1
- package/dist/app/events.js +7 -2
- package/dist/app/events.js.map +1 -1
- package/dist/app/history.d.ts +128 -0
- package/dist/app/history.d.ts.map +1 -0
- package/dist/app/history.js +269 -0
- package/dist/app/history.js.map +1 -0
- package/dist/app/identity.d.ts +33 -0
- package/dist/app/identity.d.ts.map +1 -1
- package/dist/app/identity.js +72 -0
- package/dist/app/identity.js.map +1 -1
- package/dist/app/main.js +12 -0
- package/dist/app/main.js.map +1 -1
- package/dist/app/pages.d.ts +17 -0
- package/dist/app/pages.d.ts.map +1 -1
- package/dist/app/pages.js +112 -5
- package/dist/app/pages.js.map +1 -1
- package/dist/app/review-process.d.ts +66 -0
- package/dist/app/review-process.d.ts.map +1 -0
- package/dist/app/review-process.js +199 -0
- package/dist/app/review-process.js.map +1 -0
- package/dist/app/review-worker.d.ts +2 -0
- package/dist/app/review-worker.d.ts.map +1 -0
- package/dist/app/review-worker.js +108 -0
- package/dist/app/review-worker.js.map +1 -0
- package/dist/app/review.d.ts.map +1 -1
- package/dist/app/review.js +4 -1
- package/dist/app/review.js.map +1 -1
- package/dist/app/server.d.ts +22 -2
- package/dist/app/server.d.ts.map +1 -1
- package/dist/app/server.js +77 -3
- package/dist/app/server.js.map +1 -1
- package/dist/app/sources.d.ts +68 -0
- package/dist/app/sources.d.ts.map +1 -0
- package/dist/app/sources.js +341 -0
- package/dist/app/sources.js.map +1 -0
- package/dist/ask/ask.d.ts +6 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +30 -2
- package/dist/ask/ask.js.map +1 -1
- package/dist/brain/attach.d.ts +44 -0
- package/dist/brain/attach.d.ts.map +1 -0
- package/dist/brain/attach.js +63 -0
- package/dist/brain/attach.js.map +1 -0
- package/dist/brain/connect.d.ts +59 -0
- package/dist/brain/connect.d.ts.map +1 -0
- package/dist/brain/connect.js +80 -0
- package/dist/brain/connect.js.map +1 -0
- package/dist/brain/link.d.ts +82 -0
- package/dist/brain/link.d.ts.map +1 -0
- package/dist/brain/link.js +136 -0
- package/dist/brain/link.js.map +1 -0
- package/dist/brain/push.d.ts +72 -0
- package/dist/brain/push.d.ts.map +1 -0
- package/dist/brain/push.js +191 -0
- package/dist/brain/push.js.map +1 -0
- package/dist/brain/wire.d.ts +38 -0
- package/dist/brain/wire.d.ts.map +1 -0
- package/dist/brain/wire.js +133 -0
- package/dist/brain/wire.js.map +1 -0
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +10 -3
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +32 -1
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.d.ts +5 -0
- package/dist/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +9 -1
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/session-metrics.d.ts +10 -0
- package/dist/claude/session-metrics.d.ts.map +1 -1
- package/dist/claude/session-metrics.js +25 -0
- package/dist/claude/session-metrics.js.map +1 -1
- package/dist/claude/settings-merge.d.ts +18 -0
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +42 -8
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +6 -0
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +14 -0
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/tally.d.ts +27 -0
- package/dist/claude/tally.d.ts.map +1 -1
- package/dist/claude/tally.js +74 -1
- package/dist/claude/tally.js.map +1 -1
- package/dist/cli.js +210 -3
- package/dist/cli.js.map +1 -1
- package/dist/context/price.d.ts +49 -0
- package/dist/context/price.d.ts.map +1 -0
- package/dist/context/price.js +83 -0
- package/dist/context/price.js.map +1 -0
- package/dist/context/savings.d.ts +13 -3
- package/dist/context/savings.d.ts.map +1 -1
- package/dist/context/savings.js +44 -5
- package/dist/context/savings.js.map +1 -1
- package/dist/hosts/claude-global.d.ts +33 -0
- package/dist/hosts/claude-global.d.ts.map +1 -0
- package/dist/hosts/claude-global.js +118 -0
- package/dist/hosts/claude-global.js.map +1 -0
- package/dist/hosts/plan.d.ts.map +1 -1
- package/dist/hosts/plan.js +5 -1
- package/dist/hosts/plan.js.map +1 -1
- package/dist/hosts/retract.d.ts.map +1 -1
- package/dist/hosts/retract.js +33 -13
- package/dist/hosts/retract.js.map +1 -1
- package/dist/hosts/sections.d.ts +23 -2
- package/dist/hosts/sections.d.ts.map +1 -1
- package/dist/hosts/sections.js +25 -9
- package/dist/hosts/sections.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +33 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +5 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/telemetry/gate.d.ts +14 -0
- package/dist/telemetry/gate.d.ts.map +1 -1
- package/dist/telemetry/gate.js +18 -2
- package/dist/telemetry/gate.js.map +1 -1
- package/dist/upkeep-run.d.ts.map +1 -1
- package/dist/upkeep-run.js +12 -2
- package/dist/upkeep-run.js.map +1 -1
- package/dist/upkeep.d.ts +18 -0
- package/dist/upkeep.d.ts.map +1 -1
- package/dist/upkeep.js +38 -0
- package/dist/upkeep.js.map +1 -1
- package/dist/util/state.d.ts +9 -0
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<img src="https://img.shields.io/badge/License-MIT-20C997?style=for-the-badge" />
|
|
18
18
|
<a href="TELEMETRY.md"><img src="https://img.shields.io/badge/telemetry-anonymous%2C%20opt--out-546FFF?style=for-the-badge" /></a>
|
|
19
19
|
<a href="https://scorecard.dev/viewer/?uri=github.com/NanoNets/Graft"><img src="https://img.shields.io/ossf-scorecard/github.com/NanoNets/Graft?style=for-the-badge&label=openssf%20scorecard" /></a>
|
|
20
|
-
<a href="https://
|
|
20
|
+
<a href="https://app.trailhq.com/get-started?step=pick"><img src="https://img.shields.io/badge/Trail%20Brain-try%20it-E5484D?style=for-the-badge&logoColor=white" /></a>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
### Up to **4× cheaper** and **3× faster**, with better or no loss of correctness.
|
|
@@ -31,7 +31,14 @@
|
|
|
31
31
|
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
<p align="center"
|
|
34
|
+
<p align="center">
|
|
35
|
+
<b>This works beyond code too.</b><br/>
|
|
36
|
+
A living skill file that learns from every task and gets sharper the more your team works.
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<a href="https://app.trailhq.com/get-started?step=pick"><img src="https://img.shields.io/badge/Try%20Trail%20Brain%20%E2%86%92-E5484D?style=for-the-badge" alt="Try Trail Brain" height="34"/></a>
|
|
41
|
+
</p>
|
|
35
42
|
|
|
36
43
|
<p align="center">
|
|
37
44
|
<img src="assets/graft-comparison-demo.gif" alt="Side-by-side comparison of a coding agent working with and without graft" width="820"/>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type AppCredentials, type Fetch, type RepoAccessGap } from "./identity.js";
|
|
2
|
+
import { type RepoDigest } from "./history.js";
|
|
3
|
+
/** One request to build a repository into a brain. */
|
|
4
|
+
export interface BrainBuildJob {
|
|
5
|
+
owner: string;
|
|
6
|
+
repo: string;
|
|
7
|
+
/** Branch to read; empty means the repository's default. */
|
|
8
|
+
ref?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The brain the rules land in, and the workspace key to write it with.
|
|
11
|
+
*
|
|
12
|
+
* Both optional, and omitting them changes the mode: with them, the digest is
|
|
13
|
+
* posted straight to the brain and only a job id comes back; without them the
|
|
14
|
+
* digest is RETURNED and the caller ingests it itself.
|
|
15
|
+
*
|
|
16
|
+
* The second mode is what the platform's own proxy uses. It already holds the
|
|
17
|
+
* user's session and can write to the brain directly, so handing graft a
|
|
18
|
+
* workspace key just to have it call back would mean minting a credential per
|
|
19
|
+
* build for no gain.
|
|
20
|
+
*/
|
|
21
|
+
brainId?: string;
|
|
22
|
+
brainToken?: string;
|
|
23
|
+
/** Platform base URL. Defaults to production. */
|
|
24
|
+
brainBaseUrl?: string;
|
|
25
|
+
/** File the rules as approved rather than as drafts. Onboarding sets it:
|
|
26
|
+
* a brain whose every rule is an invisible draft answers nothing. */
|
|
27
|
+
autoApprove?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface BrainBuildResult {
|
|
30
|
+
/** Set only when the digest was posted; empty in return-the-digest mode. */
|
|
31
|
+
jobId: string;
|
|
32
|
+
headSha: string;
|
|
33
|
+
commits: number;
|
|
34
|
+
threads: number;
|
|
35
|
+
symbols: number;
|
|
36
|
+
sources: number;
|
|
37
|
+
/** Set only in return-the-digest mode. */
|
|
38
|
+
digest?: RepoDigest;
|
|
39
|
+
}
|
|
40
|
+
export interface BrainBuildDeps {
|
|
41
|
+
creds: AppCredentials;
|
|
42
|
+
fetch: Fetch;
|
|
43
|
+
api?: string;
|
|
44
|
+
githubHost?: string;
|
|
45
|
+
log?: (msg: string) => void;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
}
|
|
48
|
+
/** Raised when the App cannot see the repository. Distinct because the caller
|
|
49
|
+
* turns it into a specific answer — "install the app, or run it locally" — and
|
|
50
|
+
* not into a 500. */
|
|
51
|
+
export declare class RepoNotAccessibleError extends Error {
|
|
52
|
+
/** Which gap it is, so the caller can send the user to the right place. */
|
|
53
|
+
readonly gap: RepoAccessGap;
|
|
54
|
+
constructor(message: string,
|
|
55
|
+
/** Which gap it is, so the caller can send the user to the right place. */
|
|
56
|
+
gap?: RepoAccessGap);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read the repository and hand its history to the brain.
|
|
60
|
+
*
|
|
61
|
+
* Public repositories still go through the installation lookup, because the App
|
|
62
|
+
* needs a token to clone at any useful rate limit, and a repository the App is
|
|
63
|
+
* not installed on is exactly the case the UI has to distinguish.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildRepoIntoBrain(job: BrainBuildJob, deps: BrainBuildDeps): Promise<BrainBuildResult>;
|
|
66
|
+
//# sourceMappingURL=brain-build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-build.d.ts","sourceRoot":"","sources":["../../src/app/brain-build.ts"],"names":[],"mappings":"AAiBA,OAAO,EAA0C,KAAK,cAAc,EAAE,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC5H,OAAO,EAAkE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAc/G,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;yEACqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;qBAEqB;AACrB,qBAAa,sBAAuB,SAAQ,KAAK;IAG7C,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,aAAa;gBAF3B,OAAO,EAAE,MAAM;IACf,2EAA2E;IAClE,GAAG,GAAE,aAAgF;CAIjG;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,gBAAgB,CAAC,CA4H3B"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building one repository into a brain, on demand.
|
|
3
|
+
*
|
|
4
|
+
* The whole point of the onboarding flow this serves: someone pastes a
|
|
5
|
+
* repository URL, and a minute later they are looking at rules mined from their
|
|
6
|
+
* own history. Nothing is installed and nobody has signed up yet, so this has to
|
|
7
|
+
* work from the repository name alone.
|
|
8
|
+
*
|
|
9
|
+
* Runs in the app rather than in the platform because only the app has the
|
|
10
|
+
* GitHub App credentials, only it can clone, and only graft can build a symbol
|
|
11
|
+
* graph. What crosses back to the platform is a digest of messages and symbol
|
|
12
|
+
* ids — never source.
|
|
13
|
+
*/
|
|
14
|
+
import { buildGraph } from "../graph/build.js";
|
|
15
|
+
import { contextDirFor } from "../context/node-file.js";
|
|
16
|
+
import { loadGraphCached } from "../graph/load.js";
|
|
17
|
+
import { checkoutRepository } from "./checkout.js";
|
|
18
|
+
import { appJwt, installationFor, repoAccessGap } from "./identity.js";
|
|
19
|
+
import { buildDigest, postDigest, readCommits, readSymbols, readThreads } from "./history.js";
|
|
20
|
+
import { budgetSources, readAgentInstructions, readBranchProtection, readCodeowners, readCodifiedRules, readDecisionDocs, readDeclinedIssues, readReverts, readTestNames, } from "./sources.js";
|
|
21
|
+
/** Raised when the App cannot see the repository. Distinct because the caller
|
|
22
|
+
* turns it into a specific answer — "install the app, or run it locally" — and
|
|
23
|
+
* not into a 500. */
|
|
24
|
+
export class RepoNotAccessibleError extends Error {
|
|
25
|
+
gap;
|
|
26
|
+
constructor(message,
|
|
27
|
+
/** Which gap it is, so the caller can send the user to the right place. */
|
|
28
|
+
gap = { reason: "not_installed", ownerId: null, installationId: null }) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.gap = gap;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read the repository and hand its history to the brain.
|
|
35
|
+
*
|
|
36
|
+
* Public repositories still go through the installation lookup, because the App
|
|
37
|
+
* needs a token to clone at any useful rate limit, and a repository the App is
|
|
38
|
+
* not installed on is exactly the case the UI has to distinguish.
|
|
39
|
+
*/
|
|
40
|
+
export async function buildRepoIntoBrain(job, deps) {
|
|
41
|
+
const log = deps.log ?? (() => { });
|
|
42
|
+
const api = deps.api ?? "https://api.github.com";
|
|
43
|
+
const tag = `${job.owner}/${job.repo}`;
|
|
44
|
+
const installationId = await installationFor(deps.creds, job.owner, job.repo, deps.fetch, (deps.now ?? Date.now)(), api);
|
|
45
|
+
if (installationId === null) {
|
|
46
|
+
// Which of the two gaps it is decides what the UI can offer, so it is
|
|
47
|
+
// resolved here rather than guessed there.
|
|
48
|
+
const gap = await repoAccessGap(deps.creds, job.owner, deps.fetch, (deps.now ?? Date.now)(), api);
|
|
49
|
+
throw new RepoNotAccessibleError(gap.reason === "repo_not_selected"
|
|
50
|
+
? `graft is installed on ${job.owner} but ${tag} is not in the list of repositories it can see`
|
|
51
|
+
: `graft is not installed on ${job.owner}`, gap);
|
|
52
|
+
}
|
|
53
|
+
const tokenRes = await deps.fetch(`${api}/app/installations/${installationId}/access_tokens`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
authorization: `Bearer ${appJwt(deps.creds, (deps.now ?? Date.now)())}`,
|
|
57
|
+
accept: "application/vnd.github+json",
|
|
58
|
+
"user-agent": "graft-app",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (!tokenRes.ok) {
|
|
62
|
+
throw new Error(`installation token for ${tag} failed: ${tokenRes.status}`);
|
|
63
|
+
}
|
|
64
|
+
const token = JSON.parse(await tokenRes.text()).token;
|
|
65
|
+
// Whether the repository is private decides what the UI may show before
|
|
66
|
+
// signup, and the default branch is not discoverable from a shallow
|
|
67
|
+
// single-ref fetch (there is no origin/HEAD to resolve), so both are read from
|
|
68
|
+
// the API in one call rather than guessed.
|
|
69
|
+
const meta = await repoMeta(job.owner, job.repo, token, deps.fetch, api);
|
|
70
|
+
const checkout = checkoutRepository({
|
|
71
|
+
owner: job.owner,
|
|
72
|
+
repo: job.repo,
|
|
73
|
+
ref: job.ref || meta.defaultBranch,
|
|
74
|
+
token,
|
|
75
|
+
api: deps.githubHost,
|
|
76
|
+
log,
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
// `graphOnly`: the symbol ids and their hashes are all the digest needs, and
|
|
80
|
+
// the markdown projections cost real time on a large repo.
|
|
81
|
+
await buildGraph(checkout.dir, { graphOnly: true });
|
|
82
|
+
const graph = loadGraphCached(contextDirFor(checkout.dir));
|
|
83
|
+
const commits = readCommits(checkout.dir);
|
|
84
|
+
const symbols = readSymbols(graph);
|
|
85
|
+
// The threads are the slow part (two API calls per pull request), and the
|
|
86
|
+
// one most likely to fail on a rate limit. A failure here degrades to a
|
|
87
|
+
// commits-only ingest rather than losing the whole build.
|
|
88
|
+
let threads = [];
|
|
89
|
+
try {
|
|
90
|
+
threads = await readThreads(job.owner, job.repo, token, deps.fetch, api);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
log(`${tag}: pull-request discussion unavailable (${e instanceof Error ? e.message : e}); mining commits only`);
|
|
94
|
+
}
|
|
95
|
+
// Everything the repo already states as a rule. All best-effort: the file
|
|
96
|
+
// readers cannot fail the build, and the two API readers swallow their own
|
|
97
|
+
// errors, so a repo with none of this still gets a brain from its history.
|
|
98
|
+
const sources = budgetSources([
|
|
99
|
+
...readAgentInstructions(checkout.dir),
|
|
100
|
+
...readDecisionDocs(checkout.dir),
|
|
101
|
+
...readCodeowners(checkout.dir),
|
|
102
|
+
...readCodifiedRules(checkout.dir),
|
|
103
|
+
...readReverts(checkout.dir),
|
|
104
|
+
...readTestNames(symbols.map((s) => ({ name: s.name, path: s.path }))),
|
|
105
|
+
...(await readBranchProtection(job.owner, job.repo, meta.defaultBranch, token, deps.fetch, api)),
|
|
106
|
+
...(await readDeclinedIssues(job.owner, job.repo, token, deps.fetch, api)),
|
|
107
|
+
]);
|
|
108
|
+
log(`${tag}: read ${commits.length} commits, ${threads.length} threads, ${symbols.length} symbols, ${sources.length} stated sources`);
|
|
109
|
+
const digest = buildDigest({
|
|
110
|
+
owner: job.owner,
|
|
111
|
+
name: job.repo,
|
|
112
|
+
headSha: checkout.headSha,
|
|
113
|
+
defaultBranch: job.ref || meta.defaultBranch,
|
|
114
|
+
isPrivate: meta.isPrivate,
|
|
115
|
+
commits,
|
|
116
|
+
threads,
|
|
117
|
+
symbols,
|
|
118
|
+
sources,
|
|
119
|
+
autoApprove: job.autoApprove ?? false,
|
|
120
|
+
});
|
|
121
|
+
const counts = {
|
|
122
|
+
headSha: checkout.headSha,
|
|
123
|
+
commits: commits.length,
|
|
124
|
+
threads: threads.length,
|
|
125
|
+
symbols: symbols.length,
|
|
126
|
+
sources: sources.length,
|
|
127
|
+
};
|
|
128
|
+
if (!job.brainId || !job.brainToken) {
|
|
129
|
+
return { jobId: "", ...counts, digest };
|
|
130
|
+
}
|
|
131
|
+
const { jobId } = await postDigest(job.brainBaseUrl ?? "https://agents.nanonets.com", job.brainId, job.brainToken, digest, deps.fetch);
|
|
132
|
+
return { jobId, ...counts };
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
// Always: the clone was made with an installation token and is not something
|
|
136
|
+
// to leave in /tmp.
|
|
137
|
+
checkout.cleanup();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The repository's visibility and default branch, in one call.
|
|
142
|
+
*
|
|
143
|
+
* Unknown counts as private: saying a repo is public when it is not would leak
|
|
144
|
+
* its name into a pre-signup screen. An unknown default branch is left empty,
|
|
145
|
+
* and the checkout then falls back to whatever the remote's HEAD points at,
|
|
146
|
+
* which is the same thing by another route.
|
|
147
|
+
*/
|
|
148
|
+
async function repoMeta(owner, repo, token, fetchImpl, api) {
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetchImpl(`${api}/repos/${owner}/${repo}`, {
|
|
151
|
+
headers: {
|
|
152
|
+
authorization: `Bearer ${token}`,
|
|
153
|
+
accept: "application/vnd.github+json",
|
|
154
|
+
"user-agent": "graft-app",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok)
|
|
158
|
+
return { isPrivate: true, defaultBranch: "" };
|
|
159
|
+
const parsed = JSON.parse(await res.text());
|
|
160
|
+
return {
|
|
161
|
+
isPrivate: parsed.private !== false,
|
|
162
|
+
defaultBranch: (parsed.default_branch ?? "").trim(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { isPrivate: true, defaultBranch: "" };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=brain-build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-build.js","sourceRoot":"","sources":["../../src/app/brain-build.ts"],"names":[],"mappings":"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"}
|
package/dist/app/checkout.d.ts
CHANGED
|
@@ -5,24 +5,74 @@ export interface CheckoutRequest {
|
|
|
5
5
|
baseRef: string;
|
|
6
6
|
token: string;
|
|
7
7
|
api?: string;
|
|
8
|
+
/** Where to say which of GitHub's two PR refs the review is actually using —
|
|
9
|
+
* the App's own log callback, so the two cases are told apart in the container
|
|
10
|
+
* logs rather than guessed at from the comment. */
|
|
11
|
+
log?: (msg: string) => void;
|
|
8
12
|
}
|
|
9
13
|
export interface Checkout {
|
|
10
|
-
/** Working tree at the pull request's merge commit
|
|
14
|
+
/** Working tree at the pull request's merge commit, or at its head commit when
|
|
15
|
+
* the merge ref is gone (see {@link ref}). */
|
|
11
16
|
dir: string;
|
|
12
17
|
/** What `blast --base` should diff against. */
|
|
13
18
|
base: string;
|
|
19
|
+
/** Which ref the tree came from. `merge` is the normal case; `head` means the
|
|
20
|
+
* pull request is closed and GitHub has deleted its merge preview. */
|
|
21
|
+
ref: "merge" | "head";
|
|
14
22
|
/** Removes the tree. Always call it — the token's clone is not something to
|
|
15
23
|
* leave in /tmp. */
|
|
16
24
|
cleanup: () => void;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
|
-
* Fetch the PR's
|
|
27
|
+
* Fetch the PR's code and its base, shallow, and land on it.
|
|
20
28
|
*
|
|
21
29
|
* `refs/pull/N/merge` is GitHub's own merge of the PR into its base — the same
|
|
22
30
|
* thing the checks see, and the right thing to review: it is what will land, not
|
|
23
|
-
* what the contributor's branch says in isolation.
|
|
31
|
+
* what the contributor's branch says in isolation. It is preferred whenever it
|
|
32
|
+
* exists.
|
|
33
|
+
*
|
|
34
|
+
* But GitHub DELETES that ref the moment a pull request is closed or merged: it
|
|
35
|
+
* is only a preview of a merge, and there is nothing left to preview afterwards.
|
|
36
|
+
* Re-reviewing any merged PR therefore died at the fetch with `couldn't find
|
|
37
|
+
* remote ref refs/pull/N/merge` — 16 in a row on one installation — which is why
|
|
38
|
+
* `refs/pull/N/head` (the branch tip as the author pushed it, kept indefinitely
|
|
39
|
+
* in the BASE repository, so it survives even the fork being deleted) is fetched
|
|
40
|
+
* as a fallback. What changes with it is the diff basis, which
|
|
41
|
+
* {@link headDiffBase} is entirely about.
|
|
24
42
|
*/
|
|
25
43
|
export declare function checkoutPullRequest(req: CheckoutRequest): Checkout;
|
|
44
|
+
/** What a plain-repository checkout needs: no pull request, just a ref. */
|
|
45
|
+
export interface RepoCheckoutRequest {
|
|
46
|
+
owner: string;
|
|
47
|
+
repo: string;
|
|
48
|
+
/** Branch to read. Empty means "whatever HEAD points at" (the default branch). */
|
|
49
|
+
ref?: string;
|
|
50
|
+
token: string;
|
|
51
|
+
/** How much history to fetch. The default is deeper than a PR review needs
|
|
52
|
+
* because commit MESSAGES are the payload here, not a diff — 50 commits is a
|
|
53
|
+
* fortnight on an active repo and would mine almost nothing. */
|
|
54
|
+
depth?: number;
|
|
55
|
+
api?: string;
|
|
56
|
+
log?: (msg: string) => void;
|
|
57
|
+
}
|
|
58
|
+
/** A plain checkout: the tree, the commit it landed on, and the cleanup. */
|
|
59
|
+
export interface RepoCheckout {
|
|
60
|
+
dir: string;
|
|
61
|
+
headSha: string;
|
|
62
|
+
/** The branch actually checked out, resolved when the caller named none. */
|
|
63
|
+
branch: string;
|
|
64
|
+
cleanup: () => void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clone one repository at a ref, shallow, for reading rather than reviewing.
|
|
68
|
+
*
|
|
69
|
+
* Separate from {@link checkoutPullRequest} rather than a flag on it: that
|
|
70
|
+
* function's whole shape — two refspecs, the merge/head fallback, the diff-base
|
|
71
|
+
* resolution — exists to answer "what does this pull request change", and none
|
|
72
|
+
* of it applies to "read this repository's history". Sharing the hardened `git`
|
|
73
|
+
* runner is the part worth reusing.
|
|
74
|
+
*/
|
|
75
|
+
export declare function checkoutRepository(req: RepoCheckoutRequest): RepoCheckout;
|
|
26
76
|
/** Never let a token reach a log line, even inside git's own error text. */
|
|
27
77
|
export declare function redact(text: string, token: string): string;
|
|
28
78
|
//# sourceMappingURL=checkout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../src/app/checkout.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../src/app/checkout.ts"],"names":[],"mappings":"AAoCA,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;uDAEmD;IACnD,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,QAAQ;IACvB;kDAC8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb;0EACsE;IACtE,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC;IACtB;wBACoB;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AA+CD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,GAAG,QAAQ,CA2DlE;AAED,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;oEAEgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,YAAY,CAqCzE;AAyED,4EAA4E;AAC5E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI1D"}
|
package/dist/app/checkout.js
CHANGED
|
@@ -21,6 +21,16 @@ import { tmpdir } from "node:os";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
/** Enough history for a merge base against the pull request's base branch. */
|
|
23
23
|
const FETCH_DEPTH = 50;
|
|
24
|
+
/**
|
|
25
|
+
* One extra reach back, used only on the `/head` fallback below.
|
|
26
|
+
*
|
|
27
|
+
* An open PR's base tip is a commit or two from the merge base, so 50 is plenty.
|
|
28
|
+
* A pull request being re-reviewed weeks after it merged is the opposite case:
|
|
29
|
+
* the base branch has moved on by however many commits the repo lands in that
|
|
30
|
+
* time, and the commit the branch was actually merged on top of sits behind all
|
|
31
|
+
* of them. Deepening once is cheaper than raising `FETCH_DEPTH` for every review.
|
|
32
|
+
*/
|
|
33
|
+
const DEEPEN_DEPTH = 500;
|
|
24
34
|
const GIT_TIMEOUT_MS = 120_000;
|
|
25
35
|
/** A git invocation with the dangerous parts of the environment removed. */
|
|
26
36
|
function git(dir, args, token) {
|
|
@@ -47,48 +57,206 @@ function git(dir, args, token) {
|
|
|
47
57
|
HOME: dir,
|
|
48
58
|
},
|
|
49
59
|
});
|
|
50
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
ok: res.status === 0,
|
|
62
|
+
out: res.stdout ?? "",
|
|
63
|
+
err: `${res.stderr ?? ""}${res.error ? ` ${res.error.message}` : ""}`.trim(),
|
|
64
|
+
};
|
|
51
65
|
}
|
|
66
|
+
/** First line of a git command's output, or null when it said nothing useful. */
|
|
67
|
+
function line(dir, args) {
|
|
68
|
+
const { ok, out } = git(dir, args);
|
|
69
|
+
const first = out.split("\n")[0]?.trim() ?? "";
|
|
70
|
+
return ok && first !== "" ? first : null;
|
|
71
|
+
}
|
|
72
|
+
const short = (sha) => sha.slice(0, 7);
|
|
52
73
|
/**
|
|
53
|
-
* Fetch the PR's
|
|
74
|
+
* Fetch the PR's code and its base, shallow, and land on it.
|
|
54
75
|
*
|
|
55
76
|
* `refs/pull/N/merge` is GitHub's own merge of the PR into its base — the same
|
|
56
77
|
* thing the checks see, and the right thing to review: it is what will land, not
|
|
57
|
-
* what the contributor's branch says in isolation.
|
|
78
|
+
* what the contributor's branch says in isolation. It is preferred whenever it
|
|
79
|
+
* exists.
|
|
80
|
+
*
|
|
81
|
+
* But GitHub DELETES that ref the moment a pull request is closed or merged: it
|
|
82
|
+
* is only a preview of a merge, and there is nothing left to preview afterwards.
|
|
83
|
+
* Re-reviewing any merged PR therefore died at the fetch with `couldn't find
|
|
84
|
+
* remote ref refs/pull/N/merge` — 16 in a row on one installation — which is why
|
|
85
|
+
* `refs/pull/N/head` (the branch tip as the author pushed it, kept indefinitely
|
|
86
|
+
* in the BASE repository, so it survives even the fork being deleted) is fetched
|
|
87
|
+
* as a fallback. What changes with it is the diff basis, which
|
|
88
|
+
* {@link headDiffBase} is entirely about.
|
|
58
89
|
*/
|
|
59
90
|
export function checkoutPullRequest(req) {
|
|
60
91
|
const host = (req.api ?? "https://github.com").replace(/\/$/, "");
|
|
61
92
|
const dir = mkdtempSync(join(tmpdir(), `graft-app-${req.owner}-${req.repo}-`));
|
|
62
93
|
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
63
|
-
const
|
|
94
|
+
const log = req.log ?? (() => { });
|
|
95
|
+
const tag = `${req.owner}/${req.repo}#${req.number}`;
|
|
96
|
+
const fail = (step, err) => {
|
|
97
|
+
cleanup();
|
|
98
|
+
// A merge ref is absent while GitHub is still computing mergeability, and
|
|
99
|
+
// present-but-stale right after a push — worth saying which step failed so
|
|
100
|
+
// that case is distinguishable from a permissions problem.
|
|
101
|
+
throw new Error(`checkout ${tag} failed at ${step}: ${redact(err, req.token)}`);
|
|
102
|
+
};
|
|
103
|
+
const setup = [
|
|
64
104
|
["init", ["init", "--quiet", "-b", "__graft_base"]],
|
|
65
105
|
["remote", ["remote", "add", "origin", `${host}/${req.owner}/${req.repo}.git`]],
|
|
66
|
-
// Both refs in one fetch: the merge ref to review, the base to diff against.
|
|
67
|
-
[
|
|
68
|
-
"fetch",
|
|
69
|
-
[
|
|
70
|
-
"fetch",
|
|
71
|
-
"--quiet",
|
|
72
|
-
`--depth=${FETCH_DEPTH}`,
|
|
73
|
-
"--no-recurse-submodules",
|
|
74
|
-
"origin",
|
|
75
|
-
`+refs/pull/${req.number}/merge:refs/graft/merge`,
|
|
76
|
-
`+refs/heads/${req.baseRef}:refs/graft/base`,
|
|
77
|
-
],
|
|
78
|
-
],
|
|
79
|
-
["checkout", ["checkout", "--quiet", "--detach", "refs/graft/merge"]],
|
|
80
106
|
];
|
|
81
|
-
for (const [name, args] of
|
|
82
|
-
const { ok, err } = git(dir, args
|
|
83
|
-
if (!ok)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
for (const [name, args] of setup) {
|
|
108
|
+
const { ok, err } = git(dir, args);
|
|
109
|
+
if (!ok)
|
|
110
|
+
fail(name, err);
|
|
111
|
+
}
|
|
112
|
+
// Both refs in one fetch: the ref to review, the base to diff against.
|
|
113
|
+
const merge = git(dir, fetchArgs(req, "merge"), req.token);
|
|
114
|
+
const ref = merge.ok ? "merge" : "head";
|
|
115
|
+
if (!merge.ok) {
|
|
116
|
+
const head = git(dir, fetchArgs(req, "head"), req.token);
|
|
117
|
+
// Neither ref: a base branch that has since been deleted, an installation
|
|
118
|
+
// that lost access, a repository that is gone. Both errors go out, because a
|
|
119
|
+
// bare "couldn't find refs/pull/N/merge" now means only "this PR is closed"
|
|
120
|
+
// and says nothing about why the fallback did not save it either.
|
|
121
|
+
if (!head.ok) {
|
|
122
|
+
fail("fetch", `${merge.err} | falling back to refs/pull/${req.number}/head: ${head.err}`);
|
|
89
123
|
}
|
|
90
124
|
}
|
|
91
|
-
|
|
125
|
+
const co = git(dir, ["checkout", "--quiet", "--detach", `refs/graft/${ref}`]);
|
|
126
|
+
if (!co.ok)
|
|
127
|
+
fail("checkout", co.err);
|
|
128
|
+
if (ref === "merge") {
|
|
129
|
+
log(`${tag}: reviewing refs/pull/${req.number}/merge against ${req.baseRef}`);
|
|
130
|
+
return { dir, base: "refs/graft/base", ref, cleanup };
|
|
131
|
+
}
|
|
132
|
+
const against = headDiffBase(dir, req);
|
|
133
|
+
if (against === null) {
|
|
134
|
+
fail("diff base", `refs/pull/${req.number}/merge is gone and refs/pull/${req.number}/head gives no diff against ` +
|
|
135
|
+
`${req.baseRef}: within ${FETCH_DEPTH + DEEPEN_DEPTH} commits they share no history, or the branch ` +
|
|
136
|
+
`is already in ${req.baseRef} with no merge commit to diff against (a fast-forward merge)`);
|
|
137
|
+
}
|
|
138
|
+
// Not "the PR is closed": that is only the usual reason the merge ref is
|
|
139
|
+
// missing, and stating it as fact would send an operator chasing the wrong
|
|
140
|
+
// thing when the fetch failed for some other reason.
|
|
141
|
+
log(`${tag}: no refs/pull/${req.number}/merge (GitHub deletes it when a pull request closes), reviewing refs/pull/${req.number}/head against ${against}`);
|
|
142
|
+
return { dir, base: "refs/graft/base", ref, cleanup };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Clone one repository at a ref, shallow, for reading rather than reviewing.
|
|
146
|
+
*
|
|
147
|
+
* Separate from {@link checkoutPullRequest} rather than a flag on it: that
|
|
148
|
+
* function's whole shape — two refspecs, the merge/head fallback, the diff-base
|
|
149
|
+
* resolution — exists to answer "what does this pull request change", and none
|
|
150
|
+
* of it applies to "read this repository's history". Sharing the hardened `git`
|
|
151
|
+
* runner is the part worth reusing.
|
|
152
|
+
*/
|
|
153
|
+
export function checkoutRepository(req) {
|
|
154
|
+
const host = (req.api ?? "https://github.com").replace(/\/$/, "");
|
|
155
|
+
const dir = mkdtempSync(join(tmpdir(), `graft-repo-${req.owner}-${req.repo}-`));
|
|
156
|
+
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
const log = req.log ?? (() => { });
|
|
158
|
+
const tag = `${req.owner}/${req.repo}`;
|
|
159
|
+
const fail = (step, err) => {
|
|
160
|
+
cleanup();
|
|
161
|
+
throw new Error(`${tag}: ${step} failed: ${redact(err, req.token)}`);
|
|
162
|
+
};
|
|
163
|
+
const init = git(dir, ["init", "--quiet", "--initial-branch=graft-tmp"]);
|
|
164
|
+
if (!init.ok)
|
|
165
|
+
fail("init", init.err);
|
|
166
|
+
const remote = git(dir, ["remote", "add", "origin", `${host}/${req.owner}/${req.repo}.git`]);
|
|
167
|
+
if (!remote.ok)
|
|
168
|
+
fail("remote", remote.err);
|
|
169
|
+
const depth = req.depth ?? REPO_FETCH_DEPTH;
|
|
170
|
+
// A named branch is fetched by name; with none, `HEAD` resolves to whatever
|
|
171
|
+
// the remote's default branch is, which is what the caller means by "the
|
|
172
|
+
// repository" and saves an API round trip to look the name up.
|
|
173
|
+
const refspec = req.ref
|
|
174
|
+
? `+refs/heads/${req.ref}:refs/graft/head`
|
|
175
|
+
: `+HEAD:refs/graft/head`;
|
|
176
|
+
const fetched = git(dir, ["fetch", "--quiet", `--depth=${depth}`, "--no-recurse-submodules", "origin", refspec], req.token);
|
|
177
|
+
if (!fetched.ok)
|
|
178
|
+
fail("fetch", fetched.err);
|
|
179
|
+
const co = git(dir, ["checkout", "--quiet", "--detach", "refs/graft/head"]);
|
|
180
|
+
if (!co.ok)
|
|
181
|
+
fail("checkout", co.err);
|
|
182
|
+
const headSha = line(dir, ["rev-parse", "HEAD"]) ?? "";
|
|
183
|
+
const branch = req.ref ?? line(dir, ["rev-parse", "--abbrev-ref", "origin/HEAD"]) ?? "";
|
|
184
|
+
log(`${tag}: read ${req.ref || "HEAD"} at ${short(headSha)} (${depth} commits deep)`);
|
|
185
|
+
return { dir, headSha, branch: branch.replace(/^origin\//, ""), cleanup };
|
|
186
|
+
}
|
|
187
|
+
/** Default history depth for a repository read. See RepoCheckoutRequest.depth. */
|
|
188
|
+
const REPO_FETCH_DEPTH = 500;
|
|
189
|
+
/** The one fetch each attempt makes: the PR ref to review plus the base branch. */
|
|
190
|
+
function fetchArgs(req, pull, deepen = false) {
|
|
191
|
+
return [
|
|
192
|
+
"fetch",
|
|
193
|
+
"--quiet",
|
|
194
|
+
deepen ? `--deepen=${DEEPEN_DEPTH}` : `--depth=${FETCH_DEPTH}`,
|
|
195
|
+
"--no-recurse-submodules",
|
|
196
|
+
"origin",
|
|
197
|
+
`+refs/pull/${req.number}/${pull}:refs/graft/${pull}`,
|
|
198
|
+
`+refs/heads/${req.baseRef}:refs/graft/base`,
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Point `refs/graft/base` at the commit the head ref should be diffed against,
|
|
203
|
+
* and describe it for the log — or null when this history cannot answer.
|
|
204
|
+
*
|
|
205
|
+
* The two refs do NOT give the same diff, and the difference is not a detail:
|
|
206
|
+
*
|
|
207
|
+
* - `/merge` is a commit whose parents are the base tip and the head, so
|
|
208
|
+
* `refs/graft/base...HEAD` resolves its merge base to the base tip and the
|
|
209
|
+
* diff is exactly the pull request.
|
|
210
|
+
* - `/head` is the author's branch tip. While the branch is *not* in the base
|
|
211
|
+
* branch's history — an open PR, a closed-unmerged one, and also a squash or
|
|
212
|
+
* rebase merge, which land new commits with new shas — the three-dot diff
|
|
213
|
+
* still resolves to the fork point and still reports exactly the author's
|
|
214
|
+
* changes (that is what three dots is for; see `rangeArgs` in blast/diff.ts).
|
|
215
|
+
* - After a MERGE COMMIT, though, the head commit is an ancestor of the base
|
|
216
|
+
* branch, so the merge base of the two IS the head commit and
|
|
217
|
+
* `refs/graft/base...HEAD` reports that nothing changed. An empty blast
|
|
218
|
+
* radius posted with total confidence is worse than the fetch error it
|
|
219
|
+
* replaced, so that case is detected and given the basis `/merge` would have
|
|
220
|
+
* had: the first parent of the merge commit that brought the branch in, which
|
|
221
|
+
* is the base branch as it stood at the merge.
|
|
222
|
+
*/
|
|
223
|
+
function headDiffBase(dir, req) {
|
|
224
|
+
for (const deepen of [false, true]) {
|
|
225
|
+
// The shallow window is measured from each tip, so a base branch that moved
|
|
226
|
+
// on since the merge can leave the shared history out of it entirely: git
|
|
227
|
+
// then has no merge base at all and `git diff a...b` fails outright. One
|
|
228
|
+
// deepening is the difference between reviewing a months-old PR and not.
|
|
229
|
+
if (deepen && !git(dir, fetchArgs(req, "head", true), req.token).ok)
|
|
230
|
+
return null;
|
|
231
|
+
const against = resolveHeadBase(dir, req);
|
|
232
|
+
if (against !== null)
|
|
233
|
+
return against;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function resolveHeadBase(dir, req) {
|
|
238
|
+
const head = line(dir, ["rev-parse", "refs/graft/head"]);
|
|
239
|
+
const shared = line(dir, ["merge-base", "refs/graft/base", "refs/graft/head"]);
|
|
240
|
+
if (head === null || shared === null)
|
|
241
|
+
return null;
|
|
242
|
+
if (shared !== head)
|
|
243
|
+
return `${req.baseRef} (merge base ${short(shared)})`;
|
|
244
|
+
// The head commit is in the base branch: find the merge that put it there. The
|
|
245
|
+
// EARLIEST merge on the ancestry path is the one — a later one is some other
|
|
246
|
+
// branch's merge that happens to sit above it.
|
|
247
|
+
const merge = line(dir, ["rev-list", "--reverse", "--ancestry-path", "--merges", "refs/graft/head..refs/graft/base"]);
|
|
248
|
+
if (merge === null)
|
|
249
|
+
return null;
|
|
250
|
+
const parent = line(dir, ["rev-parse", `${merge}^1`]);
|
|
251
|
+
if (parent === null)
|
|
252
|
+
return null;
|
|
253
|
+
// Only worth using if it is diffable: a shallow boundary can hold the merge
|
|
254
|
+
// commit while leaving the fork point below it out of the repository.
|
|
255
|
+
if (line(dir, ["merge-base", parent, "refs/graft/head"]) === null)
|
|
256
|
+
return null;
|
|
257
|
+
if (!git(dir, ["update-ref", "refs/graft/base", parent]).ok)
|
|
258
|
+
return null;
|
|
259
|
+
return `${req.baseRef} as it stood at the merge (${short(parent)})`;
|
|
92
260
|
}
|
|
93
261
|
/** Never let a token reach a log line, even inside git's own error text. */
|
|
94
262
|
export function redact(text, token) {
|