Mission control counts people's direct pushes to the default branch
Commits pushed straight to main, which no pull request carries, now count as people's changes on the day their push landed (merges and agents' commits left out). Read from each project's git.push events and the history back to each push's before.
5 files+78−110/5 viewed
| 313 | 313 | shows where you are needed in the workspace you have chosen in the | |
| 314 | 314 | sidebar, what its agents are doing, and what landed without you. | |
| 315 | 315 | ||
| 316 | − | Under the greeting, one line sums up the week, such as *Agents landed 39 | |
| 317 | − | of 47 changes this week without you*. A change landed without you when | |
| 318 | − | g1t merged it, by auto-merge or from the [merge queue](/guides/merge-queue/), | |
| 319 | − | with no person pressing merge. **Review N that need you** jumps to the | |
| 316 | + | Under the greeting, one line sums up the week, such as *Agents landed 37 | |
| 317 | + | of their 39 changes this week without you, and people landed 8 changes of | |
| 318 | + | their own*. An agent's change landed without you when g1t merged it, by | |
| 319 | + | auto-merge or from the [merge queue](/guides/merge-queue/), with no person | |
| 320 | + | pressing merge. People's changes are their merged pull requests and the | |
| 321 | + | commits they pushed straight to the default branch. **Review N that need you** jumps to the | |
| 320 | 322 | list, and **New issue** opens a new issue in the project you pick. | |
| 321 | 323 | ||
| 322 | 324 | | Across the top | What it counts | | |
| 323 | 325 | | --- | --- | | |
| 324 | 326 | | **Projects** | The workspace's projects, and how many were added this month. | | |
| 325 | 327 | | **Agents** | Agent runs going now, and the hours agents worked in the last 7 days. | | |
| 326 | − | | **Changes this week** | Pull requests merged in the last 7 days, and the change from the 7 days before. The change is left out when g1t cannot read far enough back to count it. | | |
| 327 | − | | **Landed without you** | The share of those changes that g1t merged with no person pressing merge. | | |
| 328 | + | | **Changes this week** | Pull requests merged in the last 7 days, and commits people pushed straight to the default branch, with the change from the 7 days before. The change is left out when g1t cannot read far enough back to count it. | | |
| 329 | + | | **Landed without you** | The share of agents' changes that g1t merged with no person pressing merge. People's own changes are not counted in it. | | |
| 328 | 330 | | **Need you** | What is waiting on you, and how many of those block work. | | |
| 329 | 331 | ||
| 330 | 332 | The list has three tabs. Each row opens to say more; the first is open. | |
| 356 | 358 | jobs. **By impact** puts the most urgent first; **Newest** sorts by time. | |
| 357 | 359 | ||
| 358 | 360 | On the right, **This week** charts the changes landed each day, split | |
| 359 | − | into those agents landed alone and those a person merged, with what | |
| 361 | + | by who did the work: agents on their own, agents with a person merging, | |
| 362 | + | and people (their pull requests and direct pushes, merges left out), with what | |
| 360 | 363 | agents and sandboxes cost over the same days. **Activity** lists what | |
| 361 | 364 | moved across the workspace, agents marked apart from people. The page | |
| 362 | 365 | refreshes itself while agents are at work. |
| 20 | 20 | sortRows, | |
| 21 | 21 | stallReason, | |
| 22 | 22 | summaryLine, | |
| 23 | + | pushedCommits, | |
| 23 | 24 | waitingRows, | |
| 24 | 25 | weekOf, | |
| 25 | 26 | whyFor, | |
| 171 | 172 | files: [], | |
| 172 | 173 | }); | |
| 173 | 174 | ||
| 175 | + | test("a push to the default branch counts a person's own commits, not merges or agents'", () => { | |
| 176 | + | const c = (hash: string, author: string, parents = 1) => ({ hash, author: { name: author }, parents: Array(parents).fill("p") }); | |
| 177 | + | const history = [c("e", "Chase"), c("d", "g1t"), c("m", "g1t", 2), c("b", "Chase"), c("a", "Chase")]; | |
| 178 | + | assert.deepEqual(pushedCommits(history, "a").map((x) => x.hash), ["e", "b"]); | |
| 179 | + | // Where it pointed before was not read: everything read counts. | |
| 180 | + | assert.deepEqual(pushedCommits(history, "zz").map((x) => x.hash), ["e", "b", "a"]); | |
| 181 | + | }); | |
| 182 | + | ||
| 174 | 183 | test("a change landed without a person when g1t merged it", () => { | |
| 175 | 184 | assert.ok(landedByAgents({ mergedBy: "g1t" })); | |
| 176 | 185 | assert.ok(landedByAgents({ mergedBy: "g1t" })); |
| 424 | 424 | // --- Landed ----------------------------------------------------------------- | |
| 425 | 425 | ||
| 426 | 426 | /** A merged pull request, as the week counts it. */ | |
| 427 | + | /** | |
| 428 | + | * The commits a push to the default branch brought, from the history at its | |
| 429 | + | * `after` (newest first) back to its `before`: people's own, not merges (a | |
| 430 | + | * pull request landing) and not agents'. A push whose `before` is not in | |
| 431 | + | * what was read gives what was read. | |
| 432 | + | */ | |
| 433 | + | export function pushedCommits<C extends { hash: string; parents: string[]; author: { name: string } }>( | |
| 434 | + | history: C[], | |
| 435 | + | before: string | undefined, | |
| 436 | + | ): C[] { | |
| 437 | + | const end = before ? history.findIndex((commit) => commit.hash === before) : -1; | |
| 438 | + | const brought = end === -1 ? history : history.slice(0, end); | |
| 439 | + | return brought.filter((commit) => commit.parents.length <= 1 && !isAgent(commit.author.name)); | |
| 440 | + | } | |
| 441 | + | ||
| 427 | 442 | export type Merged = { | |
| 428 | 443 | repo: RepoPath; | |
| 429 | 444 | number: number; |
| 4 | 4 | ||
| 5 | 5 | import { | |
| 6 | 6 | type Confidence, | |
| 7 | + | type G1tEvent, | |
| 7 | 8 | type Lifecycle, | |
| 8 | 9 | type Pull, | |
| 9 | 10 | type Repo, | |
| 10 | 11 | type RepoPath, | |
| 12 | + | type Viewer, | |
| 11 | 13 | REPO_ROLE_LABELS, | |
| 12 | 14 | isActiveRun, | |
| 13 | 15 | workOwner, | |
| 34 | 36 | import { | |
| 35 | 37 | type Fact, | |
| 36 | 38 | type Merged, | |
| 39 | + | pushedCommits, | |
| 37 | 40 | type NeedRow, | |
| 38 | 41 | type QuickAction, | |
| 39 | 42 | RUN_LABEL, | |
| 136 | 139 | return data<DelegateResult>({ error: null, notStarted: refused }); | |
| 137 | 140 | } | |
| 138 | 141 | ||
| 142 | + | /** Pushes to the default branch read for the week, per project, and commits read back from each. */ | |
| 143 | + | const PUSHES_READ = 40; | |
| 144 | + | const PUSH_DEPTH = 60; | |
| 145 | + | ||
| 146 | + | /** | |
| 147 | + | * The commits people pushed straight to a project's default branch in the | |
| 148 | + | * last two weeks, each with when its push landed. Merges and agents' | |
| 149 | + | * commits are left out: pull requests count those. | |
| 150 | + | */ | |
| 151 | + | async function directCommits(repo: Repo, viewer: Viewer): Promise<{ hash: string; at: string }[]> { | |
| 152 | + | const since = Date.now() - 14 * TIME.DAY; | |
| 153 | + | const pushes = (await eventLog.list({ repoId: repo.id, types: ["git.push"], limit: PUSHES_READ })).filter( | |
| 154 | + | (event): event is G1tEvent<"git.push"> => event.type === "git.push" && event.data.defaultBranch && Date.parse(event.time) >= since, | |
| 155 | + | ); | |
| 156 | + | const path = { namespace: repo.namespace, name: repo.name }; | |
| 157 | + | const read = await Promise.all( | |
| 158 | + | pushes.map(async (push) => { | |
| 159 | + | const history = await reposApi.log(path, viewer, push.data.after, PUSH_DEPTH).catch(() => null); | |
| 160 | + | return history?.ok ? pushedCommits(history.value, push.data.before).map((commit) => ({ hash: commit.hash, at: push.time })) : []; | |
| 161 | + | }), | |
| 162 | + | ); | |
| 163 | + | // A commit pushed twice (after a force push, say) counts once, at its first landing. | |
| 164 | + | const seen = new Map<string, string>(); | |
| 165 | + | for (const commit of read.flat().reverse()) if (!seen.has(commit.hash)) seen.set(commit.hash, commit.at); | |
| 166 | + | return [...seen].map(([hash, at]) => ({ hash, at })); | |
| 167 | + | } | |
| 168 | + | ||
| 139 | 169 | export async function loader({ context, request }: Route.LoaderArgs) { | |
| 140 | 170 | const viewer = getViewer(context); | |
| 141 | 171 | if (!viewer) { | |
| 177 | 207 | const chosen = (repos ?? []) | |
| 178 | 208 | .filter((repo) => slug != null && repo.namespace.toLowerCase() === slug.toLowerCase()) | |
| 179 | 209 | .slice(0, MAX_PROJECTS); | |
| 180 | − | const [batch, logs] = await Promise.all([ | |
| 210 | + | const [batch, logs, pushes] = await Promise.all([ | |
| 181 | 211 | work.pullsForRepos(chosen.map((repo) => repo.id), viewer, PULL_PAGE).catch(() => []), | |
| 182 | 212 | Promise.all(chosen.map((repo) => eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null))), | |
| 213 | + | // People's pushes straight to the default branch, which no pull | |
| 214 | + | // request counts: the commits each brought, for the week. | |
| 215 | + | Promise.all(chosen.map((repo) => directCommits(repo, viewer).catch(() => []))), | |
| 183 | 216 | ]); | |
| 184 | 217 | const byId = new Map(batch.map((entry) => [entry.repoId, entry])); | |
| 185 | 218 | return Promise.all( | |
| 186 | 219 | chosen.map(async (repo, index) => { | |
| 187 | 220 | const found = byId.get(repo.id); | |
| 188 | − | if (found) return { repo, pulls: found.open.slice(0, 60), closed: found.closed, events: logs[index] }; | |
| 221 | + | if (found) return { repo, pulls: found.open.slice(0, 60), closed: found.closed, events: logs[index], direct: pushes[index] ?? [] }; | |
| 189 | 222 | // A fork, which the batch leaves out: asked on its own. | |
| 190 | 223 | const path = { namespace: repo.namespace, name: repo.name }; | |
| 191 | 224 | const [pulls, closed] = await Promise.all([ | |
| 197 | 230 | pulls: pulls?.ok ? pulls.value.slice(0, 60) : null, | |
| 198 | 231 | closed: closed?.ok ? closed.value : null, | |
| 199 | 232 | events: logs[index], | |
| 233 | + | direct: pushes[index] ?? [], | |
| 200 | 234 | }; | |
| 201 | 235 | }), | |
| 202 | 236 | ); | |
| 519 | 553 | ); | |
| 520 | 554 | const twoWeeksAgo = now - 14 * TIME.DAY; | |
| 521 | 555 | const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE)); | |
| 522 | − | const week = weekOf(merged, now, tz, complete); | |
| 556 | + | // A person's commits pushed straight to the default branch are their own | |
| 557 | + | // changes too, on the day they landed. | |
| 558 | + | const direct = (perRepo ?? []).flatMap(({ direct }) => | |
| 559 | + | direct.map((commit) => ({ mergedAt: commit.at, mergedBy: null, authoredByAgent: false })), | |
| 560 | + | ); | |
| 561 | + | const week = weekOf([...merged, ...direct], now, tz, complete); | |
| 523 | 562 | const landed = landedToday(merged, now, tz); | |
| 524 | 563 | ||
| 525 | 564 | // --- Activity ------------------------------------------------------------- |
| 105 | 105 | * One branch moved by a push. `ref` is the full ref, `after` the commit it | |
| 106 | 106 | * points to now, and `defaultBranch` whether it is the default branch. | |
| 107 | 107 | */ | |
| 108 | − | "git.push": { repoId: string; ref: string; after: string; defaultBranch: boolean }; | |
| 108 | + | /** `before` is where the ref pointed before; absent for a new branch or tag. */ | |
| 109 | + | "git.push": { repoId: string; ref: string; before?: string; after: string; defaultBranch: boolean }; | |
| 109 | 110 | /** | |
| 110 | 111 | * `author` is who opened it: g1t, for one its agent filed while at work, | |
| 111 | 112 | * with `requestedBy` the person it was working for. Every issue and pull |