Mission control: a push is a person's by the account that made it, not the name on its commits
People's direct pushes to the default branch were told from g1t's by the commit author's name, so a person whose git name is "g1t" lost their commits, and g1t pushing under another name counted as a person. placePushes now reads each git.push event's actor: g1t's accounts (usr_g1t_agent, g1t_policy) and a workflow job's token (causedByJob) bring no people's commits. Within a person's push, commits g1t wrote are told by their author address (G1T_COMMIT_EMAILS), never the name. The git.push contract type gains causedByJob, which the event already carried. Tests: a person named "g1t" counts, g1t under a person's name does not, job pushes do not. Docs: mission control says how a push is counted.
5 files+95−320/5 viewed
| 566 | 566 | their own*. An agent's change landed without you when g1t merged it, by | |
| 567 | 567 | auto-merge or from the [merge queue](/guides/merge-queue/), with no person | |
| 568 | 568 | pressing merge. People's changes are their merged pull requests and the | |
| 569 | − | commits they pushed straight to the default branch. **Review N that need you** jumps to the | |
| 569 | + | commits they pushed straight to the default branch. A push is a person's | |
| 570 | + | by the account that signed in to make it, not by the name on its commits: | |
| 571 | + | pushes by g1t or a workflow job's token, and commits g1t wrote, are not | |
| 572 | + | counted as people's. **Review N that need you** jumps to the | |
| 570 | 573 | list, and **New issue** opens a new issue in the project you pick. | |
| 571 | 574 | ||
| 572 | 575 | | Across the top | What it counts | |
| 21 | 21 | sortRows, | |
| 22 | 22 | stallReason, | |
| 23 | 23 | summaryLine, | |
| 24 | + | pushedByPerson, | |
| 24 | 25 | pushedCommits, | |
| 25 | 26 | waitingRows, | |
| 26 | 27 | weekOf, | |
| ⋯ | |||
| 175 | 176 | files: [], | |
| 176 | 177 | }); | |
| 177 | 178 | ||
| 178 | − | test("a push to the default branch counts a person's own commits, not merges or agents'", () => { | |
| 179 | − | const c = (hash: string, author: string, parents = 1) => ({ hash, author: { name: author }, parents: Array(parents).fill("p") }); | |
| 180 | − | const history = [c("e", "Chase"), c("d", "g1t"), c("m", "g1t", 2), c("b", "Chase"), c("a", "Chase")]; | |
| 181 | − | assert.deepEqual(pushedCommits(history, "a").map((x) => x.hash), ["e", "b"]); | |
| 179 | + | test("a push to the default branch counts a person's own commits, not merges or g1t's", () => { | |
| 180 | + | const c = (hash: string, email: string, parents = 1) => ({ hash, author: { name: "g1t", email }, parents: Array(parents).fill("p") }); | |
| 181 | + | // Every commit is named "g1t": the name decides nothing, the address does. | |
| 182 | + | const history = [c("e", "chase@example.com"), c("d", "agent@g1t.sh"), c("m", "chase@example.com", 2), c("b", "chase@example.com"), c("a", "chase@example.com")]; | |
| 183 | + | const byG1t = (commit: { author: { email: string } }) => commit.author.email === "agent@g1t.sh"; | |
| 184 | + | assert.deepEqual(pushedCommits(history, "a", byG1t).map((x) => x.hash), ["e", "b"]); | |
| 182 | 185 | // Where it pointed before was not read: everything read counts. | |
| 183 | − | assert.deepEqual(pushedCommits(history, "zz").map((x) => x.hash), ["e", "b", "a"]); | |
| 186 | + | assert.deepEqual(pushedCommits(history, "zz", byG1t).map((x) => x.hash), ["e", "b", "a"]); | |
| 187 | + | }); | |
| 188 | + | ||
| 189 | + | test("who pushed is the account that signed in, not the name on the commits", () => { | |
| 190 | + | assert.ok(pushedByPerson({ actor: "usr_chase", data: {} })); | |
| 191 | + | assert.ok(pushedByPerson({ actor: null, data: {} })); | |
| 192 | + | assert.ok(!pushedByPerson({ actor: "usr_g1t_agent", data: {} })); | |
| 193 | + | assert.ok(!pushedByPerson({ actor: "g1t_policy", data: {} })); | |
| 194 | + | // A workflow job's own token is not a person either. | |
| 195 | + | assert.ok(!pushedByPerson({ actor: "usr_chase", data: { causedByJob: "run_1" } })); | |
| 196 | + | ||
| 197 | + | const c = (hash: string, name: string) => ({ hash, parents: ["p"], author: { name, email: `${hash}@example.com` } }); | |
| 198 | + | // A person whose git name is "g1t" pushed m1; g1t pushed w1 under a person's name. | |
| 199 | + | const history = [c("w1", "Chase Pierce"), c("m1", "g1t"), c("old", "Chase Pierce")]; | |
| 200 | + | const pushes = [ | |
| 201 | + | { time: "2026-10-07T12:00:00Z", actor: "usr_g1t_agent", data: { after: "w1", before: "m1" } }, | |
| 202 | + | { time: "2026-10-05T12:00:00Z", actor: "usr_chase", data: { after: "m1", before: "old" } }, | |
| 203 | + | ]; | |
| 204 | + | assert.deepEqual(placePushes(history, pushes), [{ hash: "m1", at: "2026-10-05T12:00:00Z" }]); | |
| 184 | 205 | }); | |
| 185 | 206 | ||
| 186 | 207 | test("a change landed without a person when g1t merged it", () => { | |
| ⋯ | |||
| 373 | 394 | }); | |
| 374 | 395 | ||
| 375 | 396 | test("each push to the default branch places the commits it brought, at its time, from one read of the history", () => { | |
| 376 | − | const c = (hash: string, parents = ["p"], author = "Chase Pierce") => ({ hash, parents, author: { name: author } }); | |
| 397 | + | const c = (hash: string, parents = ["p"], email = "chase@example.com") => ({ hash, parents, author: { name: "Chase Pierce", email } }); | |
| 377 | 398 | // Newest first: a Wednesday push of two, a Monday push of one, a merge, and g1t's own commit. | |
| 378 | − | const history = [c("w2"), c("w1"), c("m1"), c("merge", ["a", "b"]), c("bot", ["p"], "g1t"), c("old")]; | |
| 399 | + | const history = [c("w2"), c("w1"), c("m1"), c("merge", ["a", "b"]), c("bot", ["p"], "g1t@users.noreply.g1t.sh"), c("old")]; | |
| 379 | 400 | const pushes = [ | |
| 380 | − | { time: "2026-10-07T12:00:00Z", data: { after: "w2", before: "m1" } }, | |
| 381 | − | { time: "2026-10-05T12:00:00Z", data: { after: "m1", before: "old" } }, | |
| 382 | − | { time: "2026-10-01T12:00:00Z", data: { after: "gone", before: "older" } }, | |
| 401 | + | { time: "2026-10-07T12:00:00Z", actor: "usr_chase", data: { after: "w2", before: "m1" } }, | |
| 402 | + | { time: "2026-10-05T12:00:00Z", actor: "usr_chase", data: { after: "m1", before: "old" } }, | |
| 403 | + | { time: "2026-10-01T12:00:00Z", actor: "usr_chase", data: { after: "gone", before: "older" } }, | |
| 383 | 404 | ]; | |
| 384 | − | assert.deepEqual(placePushes(history, pushes), [ | |
| 405 | + | const byG1t = (commit: { author: { email: string } }) => commit.author.email.endsWith("@users.noreply.g1t.sh"); | |
| 406 | + | assert.deepEqual(placePushes(history, pushes, byG1t), [ | |
| 385 | 407 | { hash: "m1", at: "2026-10-05T12:00:00Z" }, | |
| 386 | 408 | { hash: "w2", at: "2026-10-07T12:00:00Z" }, | |
| 387 | 409 | { hash: "w1", at: "2026-10-07T12:00:00Z" }, | |
| 432 | 432 | ||
| 433 | 433 | // --- Landed ----------------------------------------------------------------- | |
| 434 | 434 | ||
| 435 | − | /** A merged pull request, as the week counts it. */ | |
| 436 | 435 | /** | |
| 437 | − | * The commits a push to the default branch brought, from the history at its | |
| 438 | − | * `after` (newest first) back to its `before`: people's own, not merges (a | |
| 439 | − | * pull request landing) and not agents'. A push whose `before` is not in | |
| 440 | − | * what was read gives what was read. | |
| 436 | + | * The accounts g1t itself acts as on the event log: its agent, and the | |
| 437 | + | * policy that merges and pushes for it. An event's `actor` is an account | |
| 438 | + | * id, so this is what tells g1t apart, whatever name a commit carries. | |
| 439 | + | */ | |
| 440 | + | export const G1T_ACCOUNT_IDS: ReadonlySet<string> = new Set(["usr_g1t_agent", "g1t_policy"]); | |
| 441 | + | ||
| 442 | + | /** A `git.push` from the log, as far as placing its commits needs it. */ | |
| 443 | + | export type PushRecord = { | |
| 444 | + | time: string; | |
| 445 | + | /** The account that pushed; null when the log does not say. */ | |
| 446 | + | actor?: string | null; | |
| 447 | + | data: { after: string; before?: string; causedByJob?: string }; | |
| 448 | + | }; | |
| 449 | + | ||
| 450 | + | /** | |
| 451 | + | * Whether a person pushed: not g1t or one of its agents (by the account | |
| 452 | + | * that signed in to push), and not a workflow job's own token. | |
| 441 | 453 | */ | |
| 442 | − | export function pushedCommits<C extends { hash: string; parents: string[]; author: { name: string } }>( | |
| 454 | + | export function pushedByPerson(push: Pick<PushRecord, "actor" | "data">): boolean { | |
| 455 | + | if (push.data.causedByJob) return false; | |
| 456 | + | return !(push.actor && G1T_ACCOUNT_IDS.has(push.actor)); | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | /** | |
| 460 | + | * The commits a person's push to the default branch brought, from the | |
| 461 | + | * history at its `after` (newest first) back to its `before`: their own, | |
| 462 | + | * not merges (a pull request landing) and not g1t's (`byG1t`: a commit | |
| 463 | + | * g1t wrote, told by its author address, which a person may fast-forward | |
| 464 | + | * onto the branch). A push whose `before` is not in what was read gives | |
| 465 | + | * what was read. Who pushed is the caller's to decide (`pushedByPerson`); | |
| 466 | + | * a commit's author name says nothing about it. | |
| 467 | + | */ | |
| 468 | + | export function pushedCommits<C extends { hash: string; parents: string[] }>( | |
| 443 | 469 | history: C[], | |
| 444 | 470 | before: string | undefined, | |
| 471 | + | byG1t: (commit: C) => boolean = () => false, | |
| 445 | 472 | ): C[] { | |
| 446 | 473 | const end = before ? history.findIndex((commit) => commit.hash === before) : -1; | |
| 447 | 474 | const brought = end === -1 ? history : history.slice(0, end); | |
| 448 | − | return brought.filter((commit) => commit.parents.length <= 1 && !isAgent(commit.author.name)); | |
| 475 | + | return brought.filter((commit) => commit.parents.length <= 1 && !byG1t(commit)); | |
| 449 | 476 | } | |
| 450 | 477 | ||
| 451 | 478 | /** | |
| 452 | − | * Each commit of `history` (newest first) a push in `pushes` (newest | |
| 453 | − | * first) brought, at that push's time. A commit pushed twice (after a | |
| 454 | − | * force push, say) counts once, at its first landing; a push whose `after` | |
| 455 | − | * is no longer in the history (rewritten) brings nothing. | |
| 479 | + | * Each commit of `history` (newest first) a person's push in `pushes` | |
| 480 | + | * (newest first) brought, at that push's time. A commit pushed twice | |
| 481 | + | * (after a force push, say) counts once, at its first landing; a push | |
| 482 | + | * whose `after` is no longer in the history (rewritten) brings nothing, | |
| 483 | + | * and g1t's own pushes bring nothing here: pull requests count those. | |
| 456 | 484 | */ | |
| 457 | − | export function placePushes<C extends { hash: string; parents: string[]; author: { name: string } }>( | |
| 485 | + | export function placePushes<C extends { hash: string; parents: string[] }>( | |
| 458 | 486 | history: C[], | |
| 459 | − | pushes: { time: string; data: { after: string; before?: string } }[], | |
| 487 | + | pushes: PushRecord[], | |
| 488 | + | byG1t: (commit: C) => boolean = () => false, | |
| 460 | 489 | ): { hash: string; at: string }[] { | |
| 461 | 490 | const index = new Map(history.map((commit, i) => [commit.hash, i])); | |
| 462 | − | const seen = new Map<string, string>(); | |
| 491 | + | // Each commit's first landing: the time, or null when g1t landed it. | |
| 492 | + | const seen = new Map<string, string | null>(); | |
| 463 | 493 | for (const push of [...pushes].reverse()) { | |
| 464 | 494 | const start = index.get(push.data.after); | |
| 465 | 495 | if (start === undefined) continue; | |
| 466 | 496 | const end = push.data.before ? index.get(push.data.before) : undefined; | |
| 467 | − | for (const commit of pushedCommits(history.slice(start, end ?? history.length), undefined)) { | |
| 468 | − | if (!seen.has(commit.hash)) seen.set(commit.hash, push.time); | |
| 497 | + | const range = history.slice(start, end ?? history.length); | |
| 498 | + | const at = pushedByPerson(push) ? push.time : null; | |
| 499 | + | for (const commit of pushedCommits(range, undefined, byG1t)) { | |
| 500 | + | if (!seen.has(commit.hash)) seen.set(commit.hash, at); | |
| 469 | 501 | } | |
| 470 | 502 | } | |
| 471 | − | return [...seen].map(([hash, at]) => ({ hash, at })); | |
| 503 | + | return [...seen].flatMap(([hash, at]) => (at ? [{ hash, at }] : [])); | |
| 472 | 504 | } | |
| 473 | 505 | ||
| 506 | + | /** A merged pull request, as the week counts it. */ | |
| 474 | 507 | export type Merged = { | |
| 475 | 508 | repo: RepoPath; | |
| 476 | 509 | number: number; |
| 37 | 37 | stuckMinutes, | |
| 38 | 38 | waitedFor, | |
| 39 | 39 | } from "../lib/mission"; | |
| 40 | + | import { G1T_COMMIT_EMAILS, normalizeEmail } from "../lib/commit-people"; | |
| 40 | 41 | import { | |
| 41 | 42 | type Fact, | |
| 42 | 43 | type Merged, | |
| ⋯ | |||
| 187 | 188 | const path = { namespace: repo.namespace, name: repo.name }; | |
| 188 | 189 | const history = await reposApi.log(path, viewer, pushes[0].data.after, HISTORY_READ).catch(() => null); | |
| 189 | 190 | if (!history?.ok) return []; | |
| 190 | − | return placePushes(history.value, pushes); | |
| 191 | + | // g1t's own commits are told by their author address, never by name. | |
| 192 | + | return placePushes(history.value, pushes, (commit) => G1T_COMMIT_EMAILS.has(normalizeEmail(commit.author.email))); | |
| 191 | 193 | } | |
| 192 | 194 | ||
| 193 | 195 | ||
| 177 | 177 | * One branch moved by a push. `ref` is the full ref, `after` the commit it | |
| 178 | 178 | * points to now, and `defaultBranch` whether it is the default branch. | |
| 179 | 179 | */ | |
| 180 | − | /** `before` is where the ref pointed before; absent for a new branch or tag. */ | |
| 181 | − | "git.push": { repoId: string; ref: string; before?: string; after: string; defaultBranch: boolean }; | |
| 182 | 180 | /** | |
| 181 | + | * `before` is where the ref pointed before; absent for a new branch or | |
| 182 | + | * tag. `causedByJob` is set when a workflow job's token pushed: the run's id. | |
| 183 | + | */ | |
| 184 | + | "git.push": { repoId: string; ref: string; before?: string; after: string; defaultBranch: boolean; causedByJob?: string }; | |
| 185 | + | /** | |
| 183 | 186 | * `author` is who opened it: g1t, for one its agent filed while at work, | |
| 184 | 187 | * with `requestedBy` the person it was working for. Every issue and pull | |
| 185 | 188 | * request event carries both. |