Commit

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.

syntaqxcommitted Parentc7ebe2aBrowse files
5 files+78−110/5 viewed
+10−7
313313 shows where you are needed in the workspace you have chosen in the
314314 sidebar, what its agents are doing, and what landed without you.
315315
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
320322 list, and **New issue** opens a new issue in the project you pick.
321323
322324 | Across the top | What it counts |
323325 | --- | --- |
324326 | **Projects** | The workspace's projects, and how many were added this month. |
325327 | **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. |
328330 | **Need you** | What is waiting on you, and how many of those block work. |
329331
330332 The list has three tabs. Each row opens to say more; the first is open.
356358 jobs. **By impact** puts the most urgent first; **Newest** sorts by time.
357359
358360 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
360363 agents and sandboxes cost over the same days. **Activity** lists what
361364 moved across the workspace, agents marked apart from people. The page
362365 refreshes itself while agents are at work.
+9−0
2020 sortRows,
2121 stallReason,
2222 summaryLine,
23+ pushedCommits,
2324 waitingRows,
2425 weekOf,
2526 whyFor,
171172 files: [],
172173 });
173174
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+
174183 test("a change landed without a person when g1t merged it", () => {
175184 assert.ok(landedByAgents({ mergedBy: "g1t" }));
176185 assert.ok(landedByAgents({ mergedBy: "g1t" }));
+15−0
424424 // --- Landed -----------------------------------------------------------------
425425
426426 /** 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+
427442 export type Merged = {
428443 repo: RepoPath;
429444 number: number;
+42−3
44
55 import {
66 type Confidence,
7+ type G1tEvent,
78 type Lifecycle,
89 type Pull,
910 type Repo,
1011 type RepoPath,
12+ type Viewer,
1113 REPO_ROLE_LABELS,
1214 isActiveRun,
1315 workOwner,
3436 import {
3537 type Fact,
3638 type Merged,
39+ pushedCommits,
3740 type NeedRow,
3841 type QuickAction,
3942 RUN_LABEL,
136139 return data<DelegateResult>({ error: null, notStarted: refused });
137140 }
138141
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+
139169 export async function loader({ context, request }: Route.LoaderArgs) {
140170 const viewer = getViewer(context);
141171 if (!viewer) {
177207 const chosen = (repos ?? [])
178208 .filter((repo) => slug != null && repo.namespace.toLowerCase() === slug.toLowerCase())
179209 .slice(0, MAX_PROJECTS);
180− const [batch, logs] = await Promise.all([
210+ const [batch, logs, pushes] = await Promise.all([
181211 work.pullsForRepos(chosen.map((repo) => repo.id), viewer, PULL_PAGE).catch(() => []),
182212 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(() => []))),
183216 ]);
184217 const byId = new Map(batch.map((entry) => [entry.repoId, entry]));
185218 return Promise.all(
186219 chosen.map(async (repo, index) => {
187220 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] ?? [] };
189222 // A fork, which the batch leaves out: asked on its own.
190223 const path = { namespace: repo.namespace, name: repo.name };
191224 const [pulls, closed] = await Promise.all([
197230 pulls: pulls?.ok ? pulls.value.slice(0, 60) : null,
198231 closed: closed?.ok ? closed.value : null,
199232 events: logs[index],
233+ direct: pushes[index] ?? [],
200234 };
201235 }),
202236 );
519553 );
520554 const twoWeeksAgo = now - 14 * TIME.DAY;
521555 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);
523562 const landed = landedToday(merged, now, tz);
524563
525564 // --- Activity -------------------------------------------------------------
+2−1
105105 * One branch moved by a push. `ref` is the full ref, `after` the commit it
106106 * points to now, and `defaultBranch` whether it is the default branch.
107107 */
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 };
109110 /**
110111 * `author` is who opened it: g1t, for one its agent filed while at work,
111112 * with `requestedBy` the person it was working for. Every issue and pull