Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge main into stream A: coordination meets the merge queue's decisions and the CLI. get_work gives the issue's decision records newest first and its latest integration_failure decision as last_integration_failure, a merge queue failure noted in a pull request's session is noted in the live harness sessions on it or its issue too, the CLI's coordination commands use the generated operations now that the API describes them (its pending list is empty) and show get_work's definition of done, and the session, decision and coordination events sit together in the catalog, the webhook form and the docs. | 1 | #!/usr/bin/env node |
| 2 | // g1t's Claude Code hooks. Claude Code runs this file for SessionStart, | |
| 3 | // UserPromptSubmit, PostToolUse, Stop and SessionEnd, with the event as JSON | |
| 4 | // on stdin. In a working copy of a g1t repository it records the session on | |
| 5 | // g1t through the `g1t` CLI: | |
| 6 | // | |
| 7 | // SessionStart g1t session start --harness claude_code --harness-session-id ID | |
| 8 | // and, when the branch or the session names an issue, | |
| 9 | // g1t work context N, given to Claude as context | |
| 10 | // UserPromptSubmit g1t session log --kind prompt TEXT (first 2,000 characters) | |
| 11 | // PostToolUse g1t session log --kind tool_call "Tool: what it was called with" | |
| 12 | // and after a git push that reached the remote, | |
| 13 | // g1t api update_session -f id=SESSION -f head_commit=SHA | |
| 14 | // Stop g1t session heartbeat | |
| 15 | // SessionEnd g1t session end --reason REASON | |
| 16 | // | |
| 17 | // What a tool returned (tool_response) is never read or sent. Prompts are | |
| 18 | // not sent when G1T_RECORD_PROMPTS=0, and nothing is when G1T_RECORD=0. | |
| 19 | // | |
| 20 | // Every hook returns at once: the CLI calls run in a detached process of | |
| 21 | // their own, so a slow network never holds Claude up. If `g1t` is not | |
| 22 | // installed or not signed in, nothing is recorded and Claude Code says so | |
| 23 | // once. Nothing is ever installed. No dependencies; Node 18 or later. | |
| 24 | ||
| 25 | import { spawn, spawnSync } from "node:child_process"; | |
| 26 | import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; | |
| 27 | import { homedir } from "node:os"; | |
| 28 | import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path"; | |
| 29 | import { fileURLToPath } from "node:url"; | |
| 30 | ||
| 31 | /** The most of a prompt that is sent. */ | |
| 32 | export const PROMPT_LIMIT = 2000; | |
| 33 | /** The most of a tool call's summary that is sent. */ | |
| 34 | export const SUMMARY_LIMIT = 200; | |
| 35 | /** The most of an issue's work context given to Claude. */ | |
| 36 | export const CONTEXT_LIMIT = 8000; | |
| 37 | /** How long SessionStart waits for an issue's work context. */ | |
| 38 | const CONTEXT_WAIT_MS = 900; | |
| 39 | /** Exit code the g1t CLI gives when signing in is needed. */ | |
| 40 | const EXIT_AUTH = 4; | |
| 41 | ||
| 42 | const SELF = fileURLToPath(import.meta.url); | |
| 43 | ||
| 44 | // --- what to send ------------------------------------------------------ | |
| 45 | ||
| 46 | export function clip(text, limit) { | |
| 47 | const value = String(text ?? ""); | |
| 48 | return value.length > limit ? `${value.slice(0, limit - 1)}…` : value; | |
| 49 | } | |
| 50 | ||
| 51 | /** Hides what looks like a secret in a command line before it is sent. */ | |
| 52 | export function redact(text) { | |
| 53 | return String(text) | |
| 54 | .replace(/\b(g1t|ghp|gho|ghs|github_pat|glpat|xox[abpr]|sk-ant|sk|rk)[-_][A-Za-z0-9_-]{8,}/g, "$1_…") | |
| 55 | .replace(/(authorization:\s*(?:bearer|basic|token)\s+)[^\s"']+/gi, "$1…") | |
| 56 | .replace(/(\b[A-Za-z_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_?KEY|PRIVATE_KEY)[A-Za-z_]*=)("[^"]*"|'[^']*'|\S+)/gi, "$1…") | |
| 57 | .replace(/(--(?:token|password|secret)[= ])\S+/gi, "$1…") | |
| 58 | .replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/g, "$1…@"); | |
| 59 | } | |
| 60 | ||
| 61 | /** One line on what a tool was called with: never what it returned. */ | |
| 62 | export function summarizeTool(name, input, cwd) { | |
| 63 | const i = input && typeof input === "object" ? input : {}; | |
| 64 | const path = (p) => { | |
| 65 | if (typeof p !== "string") return ""; | |
| 66 | if (cwd && isAbsolute(p)) { | |
| 67 | const rel = relative(cwd, p); | |
| 68 | if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return rel.replaceAll("\\", "/"); | |
| 69 | } | |
| 70 | return p; | |
| 71 | }; | |
| 72 | let what = ""; | |
| 73 | if (typeof i.command === "string") what = i.command.split("\n")[0] + (i.command.includes("\n") ? " …" : ""); | |
| 74 | else if (i.file_path || i.notebook_path) what = path(i.file_path ?? i.notebook_path); | |
| 75 | else if (typeof i.pattern === "string") what = `${i.pattern}${i.path ? ` in ${path(i.path)}` : ""}`; | |
| 76 | else if (typeof i.url === "string") what = i.url; | |
| 77 | else if (typeof i.query === "string") what = i.query; | |
| 78 | else if (typeof i.description === "string") what = i.description; | |
| 79 | else if (typeof i.action === "string") what = i.action; | |
| 80 | const line = what ? `${name}: ${redact(what)}` : String(name); | |
| 81 | return clip(line, SUMMARY_LIMIT); | |
| 82 | } | |
| 83 | ||
| 84 | /** The issue a branch is for, when its name holds the number: 42-fix, issue-42, fix/42, g1t/42-x. */ | |
| 85 | export function issueFromBranch(branch) { | |
| 86 | if (!branch || branch === "HEAD") return null; | |
| 87 | if (/^(release|releases|hotfix|v\d)/i.test(branch)) return null; | |
| 88 | const match = /(?:^|[/_-])(?:issues?-?|#)?(\d{1,7})(?=$|[/_-])/i.exec(branch); | |
| 89 | if (!match) return null; | |
| 90 | const n = Number(match[1]); | |
| 91 | return n > 0 ? n : null; | |
| 92 | } | |
| 93 | ||
| 94 | /** The issue a shell command claims or attaches the session to, if any. */ | |
| 95 | export function issueFromCommand(command) { | |
| 96 | if (typeof command !== "string") return null; | |
| 97 | const patterns = [ | |
| 98 | /\bg1t\s+work\s+claim\s+#?(\d+)/, | |
| 99 | /\bg1t\s+session\s+(?:start|attach)\b[^|;&\n]*--issue[= ]#?(\d+)/, | |
| 100 | /\bg1t\s+pr\s+create\b[^|;&\n]*--issue[= ]#?(\d+)/, | |
| 101 | ]; | |
| 102 | for (const pattern of patterns) { | |
| 103 | const match = pattern.exec(command); | |
| 104 | if (match) return Number(match[1]); | |
| 105 | } | |
| 106 | return null; | |
| 107 | } | |
| 108 | ||
| 109 | export const isGitPush = (command) => | |
| 110 | typeof command === "string" && /(^|[\s;&|(])git(\s+-\S+(\s+[^-\s]\S*)?)*\s+push\b/.test(command); | |
| 111 | ||
| 112 | /** | |
| 113 | * What a hook event asks for: the CLI calls to run in the background | |
| 114 | * (`steps`), and whether to start the session first. Pure, so the tests | |
| 115 | * can check it without a CLI. | |
| 116 | */ | |
| 117 | export function plan(event, env = process.env) { | |
| 118 | const name = event?.hook_event_name; | |
| 119 | const sid = event?.session_id; | |
| 120 | if (!name || !sid || env.G1T_RECORD === "0") return null; | |
| 121 | const cwd = event.cwd || process.cwd(); | |
| 122 | const job = { cwd, sid, steps: [] }; | |
| 123 | switch (name) { | |
| 124 | case "SessionStart": | |
| 125 | job.start = true; | |
| 126 | return job; | |
| 127 | case "UserPromptSubmit": | |
| 128 | if (env.G1T_RECORD_PROMPTS === "0" || typeof event.prompt !== "string" || !event.prompt.trim()) return null; | |
| 129 | job.steps.push({ args: ["session", "log", "--kind", "prompt", "--", clip(event.prompt, PROMPT_LIMIT)] }); | |
| 130 | return job; | |
| 131 | case "PostToolUse": { | |
| 132 | if (!event.tool_name) return null; | |
| 133 | const input = event.tool_input ?? {}; | |
| 134 | job.steps.push({ args: ["session", "log", "--kind", "tool_call", "--", summarizeTool(event.tool_name, input, cwd)] }); | |
| 135 | if (event.tool_name === "Bash") { | |
| 136 | const issue = issueFromCommand(input.command); | |
| 137 | if (issue) job.issue = issue; | |
| 138 | if (isGitPush(input.command)) job.steps.push({ headCommit: true }); | |
| 139 | } | |
| 140 | return job; | |
| 141 | } | |
| 142 | case "Stop": | |
| 143 | job.steps.push({ args: ["session", "heartbeat"] }); | |
| 144 | return job; | |
| 145 | case "SessionEnd": | |
| 146 | job.steps.push({ args: ["session", "end", "--reason", String(event.reason || "other")] }); | |
| 147 | job.end = true; | |
| 148 | return job; | |
| 149 | default: | |
| 150 | return null; | |
| 151 | } | |
| 152 | } | |
| 153 | ||
| 154 | // --- the machine ------------------------------------------------------- | |
| 155 | ||
| 156 | function git(cwd, args) { | |
| 157 | const out = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true, timeout: 3000 }); | |
| 158 | return out.status === 0 ? out.stdout.trim() : null; | |
| 159 | } | |
| 160 | ||
| 161 | /** The g1t CLI on PATH, without running it. G1T_BIN names another. */ | |
| 162 | export function findCli(env = process.env) { | |
| 163 | if (env.G1T_BIN) return existsSync(env.G1T_BIN) ? env.G1T_BIN : null; | |
| 164 | const names = process.platform === "win32" ? ["g1t.exe", "g1t"] : ["g1t"]; | |
| 165 | for (const dir of (env.PATH ?? env.Path ?? "").split(delimiter)) { | |
| 166 | if (!dir) continue; | |
| 167 | for (const name of names) { | |
| 168 | const candidate = join(dir.replace(/^"|"$/g, ""), name); | |
| 169 | try { | |
| 170 | if (statSync(candidate).isFile()) return candidate; | |
| 171 | } catch {} | |
| 172 | } | |
| 173 | } | |
| 174 | return null; | |
| 175 | } | |
| 176 | ||
| 177 | /** Runs the CLI. A .js or .mjs G1T_BIN (the tests' stand-in) runs under Node. */ | |
| 178 | function cli(bin, args, options) { | |
| 179 | const script = /\.m?js$/.test(bin); | |
| 180 | return spawnSync(script ? process.execPath : bin, script ? [bin, ...args] : args, { | |
| 181 | encoding: "utf8", | |
| 182 | windowsHide: true, | |
| 183 | ...options, | |
| 184 | }); | |
| 185 | } | |
| 186 | ||
| 187 | /** Where the plugin keeps its state: Claude Code's data folder for it. */ | |
| 188 | export function dataDir(env = process.env) { | |
| 189 | return env.CLAUDE_PLUGIN_DATA || join(homedir(), ".g1t", "claude-code"); | |
| 190 | } | |
| 191 | ||
| 192 | function readJson(path, fallback) { | |
| 193 | try { | |
| 194 | return JSON.parse(readFileSync(path, "utf8")); | |
| 195 | } catch { | |
| 196 | return fallback; | |
| 197 | } | |
| 198 | } | |
| 199 | ||
| 200 | function write(path, text) { | |
| 201 | try { | |
| 202 | mkdirSync(dirname(path), { recursive: true }); | |
| 203 | writeFileSync(path, text); | |
| 204 | } catch {} | |
| 205 | } | |
| 206 | ||
| 207 | const readText = (path) => { | |
| 208 | try { | |
| 209 | return readFileSync(path, "utf8").trim() || null; | |
| 210 | } catch { | |
| 211 | return null; | |
| 212 | } | |
| 213 | }; | |
| 214 | ||
| 215 | /** This session's files: its marker (it is in a g1t working copy), its g1t session id and its issue. */ | |
| 216 | export function sessionFiles(sid, env = process.env) { | |
| 217 | const dir = join(dataDir(env), "sessions"); | |
| 218 | const safe = String(sid).replace(/[^A-Za-z0-9_-]/g, "_"); | |
| 219 | return { dir, marker: join(dir, `${safe}.json`), id: join(dir, `${safe}.id`), issue: join(dir, `${safe}.issue`) }; | |
| 220 | } | |
| 221 | ||
| 222 | /** Forgets sessions untouched for a week (ones that never saw SessionEnd). */ | |
| 223 | function prune(dir) { | |
| 224 | try { | |
| 225 | const cutoff = Date.now() - 7 * 24 * 3600 * 1000; | |
| 226 | for (const name of readdirSync(dir)) { | |
| 227 | const path = join(dir, name); | |
| 228 | if (statSync(path).mtimeMs < cutoff) rmSync(path, { force: true }); | |
| 229 | } | |
| 230 | } catch {} | |
| 231 | } | |
| 232 | ||
| 233 | // --- saying so once ---------------------------------------------------- | |
| 234 | ||
| 235 | const MESSAGES = { | |
| 236 | missing: | |
| 237 | "g1t: this session is not being recorded on g1t, because the g1t CLI is not installed. Install it (https://docs.g1t.sh/guides/cli/) and run `g1t auth login`. This is said once.", | |
| 238 | signed_out: | |
| 239 | "g1t: this session is not being recorded on g1t, because the g1t CLI is not signed in. Run `g1t auth login`. This is said once.", | |
| 240 | }; | |
| 241 | ||
| 242 | /** Remembers a problem (missing, signed_out), or that there is none (null). */ | |
| 243 | export function setProblem(env, problem) { | |
| 244 | const path = join(dataDir(env), "notice.json"); | |
| 245 | const notice = readJson(path, {}); | |
| 246 | if ((notice.problem ?? null) === problem) return; | |
| 247 | write(path, JSON.stringify({ problem, shown: false })); | |
| 248 | } | |
| 249 | ||
| 250 | /** The message for a problem not shown yet, marking it shown. */ | |
| 251 | export function takeNotice(env) { | |
| 252 | const path = join(dataDir(env), "notice.json"); | |
| 253 | const notice = readJson(path, {}); | |
| 254 | if (!notice.problem || notice.shown || !MESSAGES[notice.problem]) return null; | |
| 255 | write(path, JSON.stringify({ ...notice, shown: true })); | |
| 256 | return MESSAGES[notice.problem]; | |
| 257 | } | |
| 258 | ||
| 259 | /** Whether a working copy's remotes are on g1t (g1t.sh, or G1T_HOST's host). */ | |
| 260 | export function isG1tRemote(urls, env = process.env) { | |
| 261 | if (env.G1T_REPO) return true; | |
| 262 | const hosts = ["g1t.sh", env.G1T_HOST].filter(Boolean).map((h) => h.toLowerCase()); | |
| 263 | return urls.some((url) => hosts.some((host) => url.toLowerCase().includes(host))); | |
| 264 | } | |
| 265 | ||
| 266 | // --- the background half ----------------------------------------------- | |
| 267 | ||
| 268 | /** Hands a job to a detached copy of this script and returns at once. */ | |
| 269 | function background(job, env = process.env) { | |
| 270 | const payload = Buffer.from(JSON.stringify(job)).toString("base64url"); | |
| 271 | try { | |
| 272 | const child = spawn(process.execPath, [SELF, "--worker", payload], { | |
| 273 | cwd: job.cwd, | |
| 274 | detached: true, | |
| 275 | stdio: "ignore", | |
| 276 | windowsHide: true, | |
| 277 | env, | |
| 278 | }); | |
| 279 | child.on("error", () => {}); | |
| 280 | child.unref(); | |
| 281 | } catch {} | |
| 282 | } | |
| 283 | ||
| 284 | /** Starts the session if it needs to, then runs the job's steps in order. */ | |
| 285 | export function work(job, env = process.env) { | |
| 286 | const bin = findCli(env); | |
| 287 | if (!bin) return setProblem(env, "missing"); | |
| 288 | const files = sessionFiles(job.sid, env); | |
| 289 | if (job.issue) write(files.issue, String(job.issue)); | |
| 290 | const opts = { cwd: job.cwd, timeout: 20000, env: { ...env } }; | |
| 291 | let id = readText(files.id); | |
| 292 | if (job.start || !id) { | |
| 293 | const issue = job.issue ?? readText(files.issue); | |
| 294 | const args = ["session", "start", "--harness", "claude_code", "--harness-session-id", job.sid]; | |
| 295 | if (issue) args.push("--issue", String(issue)); | |
| 296 | args.push("--json"); | |
| 297 | const out = cli(bin, args, opts); | |
| 298 | if (out.status === EXIT_AUTH) return setProblem(env, "signed_out"); | |
| 299 | if (out.status !== 0) return; | |
| 300 | setProblem(env, null); | |
| 301 | try { | |
| 302 | id = JSON.parse(out.stdout).id; | |
| 303 | } catch { | |
| 304 | id = /\bses_[A-Za-z0-9]+/.exec(out.stdout ?? "")?.[0] ?? null; | |
| 305 | } | |
| 306 | if (!id) return; | |
| 307 | write(files.id, id); | |
| 308 | } | |
| 309 | opts.env.G1T_SESSION = id; | |
| 310 | for (const step of job.steps) { | |
| 311 | if (step.args) { | |
| 312 | const out = cli(bin, step.args, opts); | |
| 313 | if (out.status === EXIT_AUTH) return setProblem(env, "signed_out"); | |
| 314 | } else if (step.headCommit) { | |
| 315 | // Only once the push reached the remote: HEAD is on a remote branch. | |
| 316 | const head = git(job.cwd, ["rev-parse", "HEAD"]); | |
| 317 | const remote = git(job.cwd, ["branch", "-r", "--contains", "HEAD"]); | |
| 318 | if (head && remote) cli(bin, ["api", "update_session", "-f", `id=${id}`, "-f", `head_commit=${head}`, "--silent"], opts); | |
| 319 | } | |
| 320 | } | |
| 321 | if (job.end) for (const path of [files.marker, files.id, files.issue]) rmSync(path, { force: true }); | |
| 322 | } | |
| 323 | ||
| 324 | // --- the hooks --------------------------------------------------------- | |
| 325 | ||
| 326 | /** | |
| 327 | * SessionStart: marks the session as one in a g1t working copy, and | |
| 328 | * returns what to print: context for Claude, and a notice for you, once. | |
| 329 | * Null outside a g1t working copy, where nothing is recorded. | |
| 330 | */ | |
| 331 | export function sessionStart(event, env = process.env, bin = findCli(env)) { | |
| 332 | const cwd = event.cwd || process.cwd(); | |
| 333 | const gitDir = git(cwd, ["rev-parse", "--absolute-git-dir"]); | |
| 334 | if (!gitDir) return null; | |
| 335 | // The branch, from HEAD itself: also on a branch with no commits yet. | |
| 336 | const branch = /^ref: refs\/heads\/(.+)$/m.exec(readText(join(gitDir, "HEAD")) ?? "")?.[1] ?? null; | |
| 337 | const urls = (git(cwd, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split(/\r?\n/); | |
| 338 | if (!isG1tRemote(urls, env)) return null; | |
| 339 | ||
| 340 | const files = sessionFiles(event.session_id, env); | |
| 341 | prune(files.dir); | |
| 342 | write(files.marker, JSON.stringify({ cwd, git_dir: gitDir })); | |
| 343 | ||
| 344 | const output = {}; | |
| 345 | if (!bin) setProblem(env, "missing"); | |
| 346 | const notice = takeNotice(env); | |
| 347 | if (notice) output.systemMessage = notice; | |
| 348 | if (!bin) return output; | |
| 349 | ||
| 350 | if (env.CLAUDE_ENV_FILE) { | |
| 351 | // Claude's own `g1t` commands then act in this session, not in another | |
| 352 | // one in the same working copy. Read as each command runs, since the | |
| 353 | // session's id arrives after this hook has returned. | |
| 354 | const p = files.id.replaceAll("\\", "/"); | |
| 355 | try { | |
| 356 | writeFileSync(env.CLAUDE_ENV_FILE, `[ -f "${p}" ] && export G1T_SESSION="$(cat "${p}")"\n`, { flag: "a" }); | |
| 357 | } catch {} | |
| 358 | } | |
| 359 | ||
| 360 | const remembered = Number(readText(files.issue)) || null; | |
| 361 | const issue = remembered ?? issueFromBranch(branch); | |
| 362 | let context; | |
| 363 | if (issue) { | |
| 364 | const where = remembered ? "this session's claim" : `the branch ${branch}`; | |
| 365 | const out = cli(bin, ["work", "context", String(issue)], { cwd, timeout: CONTEXT_WAIT_MS, env }); | |
| 366 | if (out.status === 0 && out.stdout.trim()) { | |
| 367 | context = `g1t: this session is working on issue #${issue} (from ${where}). Its work context on g1t:\n\n${clip(out.stdout.trim(), CONTEXT_LIMIT)}\n\nIf it is not claimed by this session yet, claim it with \`g1t work claim ${issue}\` before you start.`; | |
| 368 | } else if (out.status === EXIT_AUTH) { | |
| 369 | setProblem(env, "signed_out"); | |
| 370 | const now = takeNotice(env); | |
| 371 | if (now) output.systemMessage = now; | |
| 372 | } else { | |
| 373 | context = `g1t: ${where} points at issue #${issue}. Load what is known about it with \`g1t work context ${issue}\`, and claim it with \`g1t work claim ${issue}\` before you start.`; | |
| 374 | } | |
| 375 | } else { | |
| 376 | context = | |
| 377 | "g1t: this is a g1t repository. Before starting on an issue, load it with `g1t work context N` and claim it with `g1t work claim N`, so the people and agents also working here know."; | |
| 378 | } | |
| 379 | if (context) output.hookSpecificOutput = { hookEventName: "SessionStart", additionalContext: context }; | |
| 380 | return output; | |
| 381 | } | |
| 382 | ||
| 383 | /** Runs one hook: what it prints, and the job it hands to the background. */ | |
| 384 | export function hook(event, env = process.env) { | |
| 385 | const job = plan(event, env); | |
| 386 | if (!job) return { output: null, job: null }; | |
| 387 | if (event.hook_event_name === "SessionStart") { | |
| 388 | const bin = findCli(env); | |
| 389 | const output = sessionStart(event, env, bin); | |
| 390 | if (!output) return { output: null, job: null }; | |
| 391 | return { output: Object.keys(output).length ? output : null, job: bin ? job : null }; | |
| 392 | } | |
| 393 | // Only sessions SessionStart found in a g1t working copy are recorded. | |
| 394 | if (!existsSync(sessionFiles(event.session_id, env).marker) || !findCli(env)) return { output: null, job: null }; | |
| 395 | const notice = event.hook_event_name === "UserPromptSubmit" ? takeNotice(env) : null; | |
| 396 | return { output: notice ? { systemMessage: notice } : null, job }; | |
| 397 | } | |
| 398 | ||
| 399 | async function main() { | |
| 400 | if (process.argv[2] === "--worker") { | |
| 401 | work(JSON.parse(Buffer.from(process.argv[3], "base64url").toString("utf8"))); | |
| 402 | return; | |
| 403 | } | |
| 404 | let raw = ""; | |
| 405 | for await (const chunk of process.stdin) raw += chunk; | |
| 406 | const { output, job } = hook(JSON.parse(raw || "{}")); | |
| 407 | if (output) process.stdout.write(JSON.stringify(output)); | |
| 408 | if (job) background(job); | |
| 409 | } | |
| 410 | ||
| 411 | if (process.argv[1] && resolve(process.argv[1]) === SELF) { | |
| 412 | main() | |
| 413 | .catch(() => {}) | |
| 414 | .finally(() => process.exit(0)); | |
| 415 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.