Review order and the verdict
Read first what could throw the most work away; approve when the code is better than main, not when it is perfect.
Predict first
Before you read: predict
MR !4770 on ExamHall, as opened. Its description: "Closes #3127. Multi-select questions now earn partial credit. My editor also re-indented src/grading/ from 4 spaces to 2." The review tool lists 638 changed lines:
src/grading/feedback.js +96 -96
src/grading/index.js +1 -0
src/grading/rubric.js +200 -200
src/grading/score.js +20 -10
src/grading/score.test.js +15 -0
You have read the description.
Which file do you open first?
The 30-second version
Read a merge request in the order its findings would cost you. Let CI's linter carry what a rule can check. Approve once the change leaves the code healthier than main, even when it is not how you would have written it.
Read first what could throw the most work away; approve when the code is better than main, not when it is perfect.
How it works
Step 1 of 13
Analogy: A council arts officer reviewing a muralist's proposal for a library's blank side wall
| At the wall | In the review |
|---|---|
| she reads the commission first: which wall, and the theme the library asked for | intent first (step 2) |
| she checks the wall is staying: the library's plans say which walls come down next year | should it happen at all (step 3) |
| she checks the full-size outline on the wall before any colour; paint on a figure the outline then moves is wasted | design before the lines (steps 4–6) |
| a proposal that also repaints the building's window frames is split into two jobs | a reformat travels alone (step 8) |
| a damp meter, not her eye, says whether the render is dry enough to paint | CI's linter (step 7) |
| she signs off on a wall better than the bare render, not on the wall she would have painted | the verdict (steps 10, 11) |
In real terms: the earliest questions are the ones whose answers can discard the most work, and the verdict asks one thing: is the code better than it was?
Breaks down when: paint is hard to undo and the next mural is years away; a merge request is cheap to revert and the next one comes tomorrow, so "better than now" is an easier bar to meet in code.
Contrasting case: a one-line typo fix has no design to question and no main part to find; one read and a verdict are the whole review.
What the sources support, and what they don't. The rules are Google's code review guide and GitLab's code review guidelines, valid as of 2026-10-15; step 1's cost order and "readability last" are this lesson's reading.
Examples, run for real
4.1 ExamHall EXH-4470: the partial-credit MR, read in order
EXH-4470: the first review of MR !4770 (the opening question's case) spent half an hour on style comments in rubric.js, the biggest file. The second review read it in order:
4.1 intent: issue #3127's example, q7 (key a, c, d; 3 marks) answered a and c: main 0 marks · MR 2 · a single-choice q1: main 2 · MR 2
4.1 diffstat main -> MR !4770: 638 changed lines in 5 files · with whitespace ignored: 34 lines in 3 files (index.js, score.js, score.test.js)
4.1 split: MR !4771 (the re-indent alone): 608 lines, 0 once whitespace is ignored · MR !4770 on top of it: 34 lines (index.js +1 -0, score.js +14 -4, score.test.js +15 -0)
The description and issue #3127 come first: the example from the issue earns 0 marks on main and 2 with the MR, and single-choice marks are unchanged, so the change does what it says (step 2). Ignoring whitespace shrinks the review from 638 lines to 34, all in score.js, its test and one export; that is where the design question lives (step 4). The reviewer asks for the re-indent as MR !4771, which needs no reading once whitespace is ignored, and MR !4770 is rebased on it (step 8).
4.2 Proof domain, chat: better than main?
A team chat's MR !4773 stops a user's own messages counting towards their unread badge. Four checks are run on main, on the MR and on the author's next push:
4.2 main: 3 of 4 checks pass · red: my own messages add nothing to my badge
4.2 MR !4773: 3 of 4 · fixed: my own messages add nothing to my badge · broken: a muted room shows no badge
4.2 verdict against main: 1 fixed, 1 broken · not yet better than main · summary note: "Just one thing to address: badgeText no longer checks room.muted, so a muted room shows a badge (2 in the test room). The unread fix itself looks good."
4.2 the author's fix: 4 of 4 · fixed 1 · broken 0 · better than main: approve
The MR fixes its bug and breaks a check that passes on main: the rewritten badgeText lost the muted-room test. It is not yet better than main, so the summary note asks for that one thing and praises the rest (steps 10, 13). After the fix nothing that worked on main is broken, and the MR is approved.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
A team chat's MR !4772 adds a typing indicator. Ana reviewed its files in the order the tool lists them and left eight line comments:
room-view.jsline 8: pass the room, not just its idtyping-indicator.jsline 2:IDLEneeds its unittyping-indicator.jsline 4: name itcreateTypingIndicatortyping-indicator.jsline 5: a template literal reads bettertyping-indicator.jsline 6:last→lastSentAttyping-indicator.jsline 9: invert theiftyping-indicator.test.jsline 6:sent→messagestyping-indicator.test.jsline 11: also check the event's fields
// src/typing/typing-indicator.js (MR !4772)
// Typing indicator: its own channel, one per room.
const IDLE = 3000;
export function startTyping(socket, roomId) {
const channel = socket.channel('typing:' + roomId);
let last = 0;
return {
keypress(userId, now) {
if (now - last < IDLE) return;
last = now;
channel.send({ userId, typing: true });
},
stop(userId) {
channel.send({ userId, typing: false });
},
};
}
Only then did she open presence.js:
// src/presence/presence.js (main, unchanged by the MR)
// Per-room presence over the app's one open socket.
export function createPresence(socket) {
return {
online(roomId, userId) {
socket.send({ kind: 'presence', roomId, userId, state: 'online' });
},
away(roomId, userId) {
socket.send({ kind: 'presence', roomId, userId, state: 'away' });
},
};
}
Her ninth comment reads: "issue (blocking): presence.js already sends per-room events over the open socket; send typing as a presence state there, rather than opening a second channel in a new module." The author agrees and pushes a rework that does exactly that. It deletes typing-indicator.js and typing-indicator.test.js and changes presence.js and room-view.js:
// src/rooms/room-view.js (the rework)
import { createPresence } from '../presence/presence.js';
// Opens a room: marks the user online and reports typing through presence.
export function openRoom(socket, room, me) {
const presence = createPresence(socket);
presence.online(room.id, me.id);
return { onKey: () => presence.typing(room.id, me.id) };
}
How many of Ana's eight line comments are still on unchanged lines after the rework?
// src/rooms/room-view.js (MR !4772)import { createPresence } from '../presence/presence.js';import { startTyping } from '../typing/typing-indicator.js'; // Opens a room: marks the user online and starts the typing indicator.export function openRoom(socket, room, me) { const presence = createPresence(socket); presence.online(room.id, me.id); const typing = startTyping(socket, room.id); return { onKey: () => typing.keypress(me.id) };}Question 2 of 8
A box office's MR !4774 adds a sweep that releases expired seat holds:
// eslint.config.js (main, unchanged by the MR)
export default [
{
files: ['src/**/*.js'],
rules: { curly: 'error', 'prefer-const': 'error', 'no-unused-vars': 'error' },
},
];
CI runs ESLint with this config on every MR and fails the pipeline on any error. You have drafted four comments:
- line 5: put the
ifbody in braces. - line 3:
releasedis never reassigned; declare it withconst. - line 5: a hold that ends exactly at
nowMsstays held; compare with<=. - line 4:
hsays little; call ithold.
Which drafts will CI already report, so you can drop them?
// src/holds/expire.js (MR !4774)// Releases every seat whose hold has ended by nowMs.export function expireHolds(holds, nowMs) { let released = []; for (const h of holds) { if (h.endsAtMs < nowMs) released.push(h.seatId); } return released;}Question 3 of 8
A video platform's MR !4775 changes only src/player/subtitles.js and src/player/subtitles.test.js, so the player falls back to a viewer's second language. You have reviewed it and found nothing to block. Its pipeline runs 9 tests and 1 fails: src/billing/renewal.test.js › "renews a 31 January subscription on 28 February". It fails the same way on every retry. renewal.test.js imports only ./renewal.js.
What do you do?
Question 4 of 8
Issue #4120 on a payments ledger: a payment of 5,000 cents already refunded 3,000 must refuse a second refund of 3,000. MR !4776 changes one function:
// src/refunds/refund.js (MR !4776)
// Refuses a refund larger than what is left of the payment after earlier refunds.
export function canRefund(payment, amountCents) {
const refunded = payment.refunds.reduce((sum, r) => sum + r.amountCents, 0);
return amountCents > 0 && amountCents <= payment.amountCents - refunded;
}
Your drafts:
- The sum of earlier refunds could live on
PaymentasrefundedCents(), so the statement page can use it too. rwould read better asrefund.- After two refunds of 2,500, can a third refund of 1 cent still go through?
What do you do next?
// src/refunds/refund.js (main)// Refuses a refund larger than the payment.export function canRefund(payment, amountCents) { return amountCents > 0 && amountCents <= payment.amountCents;}Question 5 of 8
MR !4779 lets a viewer resume each episode where they stopped. It links no issue; its description and its tests say what resuming must do. It adds a resume_positions table, an endpoint and three changes to the player.
Put the four steps of your review in order, first step first.
Your order
- 1empty
- 2empty
- 3empty
- 4empty
Tap the lines in the order you expect.
Question 6 of 8
A teammate opens a 2,000-line merge request on ExamHall's grading, written mostly with an AI assistant. Its description is one line: "Grading v2: big refactor + new rubric editor + fixes". The reply is a short text file.
Write the first reply you post on the MR, before any line comment.
Question 7 of 8
A team chat's MR adds emoji reactions to messages, stored in a new column, rx, on the messages table. Its code is clean and its tests pass. ADR-14, agreed last month, moves everything that annotates a message (edits, pins, reactions) into the new message-events service, and the messages table's extra columns are being removed this quarter. The author was on leave that month. Your first reply goes out before any line comment.
Which reply do you post?
Question 8 of 8
A box office's MR !4777 lets staff extend a seat hold. Ben reviewed readability first and left six comments:
- line 1: say how many minutes
- line 2:
EXTRA→EXTRA_MINUTES - line 4:
nowMs→now, as in the other hold functions - line 5:
60 * 1000reads better as60_000 - line 6:
t→newEnd - line 7: fold
tinto this line
Then he found the bug: an extension starts from now, not from the hold's current end, so a hold extended early gains less than 10 minutes, or even ends sooner. The author's fix:
// src/holds/extend.js (the author's fix)
// Extends a seat hold by the box office's extra minutes.
const EXTRA = 10;
export function extendHold(hold, nowMs) {
const endsAtMs = Math.max(hold.endsAtMs, nowMs) + EXTRA * 60 * 1000;
return { ...hold, endsAtMs };
}
How many of Ben's six comments are still on unchanged lines after the fix?
// src/holds/extend.js (MR !4777)// Extends a seat hold by the box office's extra minutes.const EXTRA = 10; export function extendHold(hold, nowMs) { const ms = EXTRA * 60 * 1000; const t = nowMs + ms; hold.endsAtMs = t; return hold;}Go deeper
Pro tip
- Read the test names before the code. They say what the change promises in one line each; then the main file has to keep those promises. When: any MR whose tests changed (steps 2, 4). Google's guide: "Sometimes it's also helpful to read the tests first before you read the main code". Verify (RAN,
tips.mjs):
Status: RAN.tip score.test.js, names in file order: a single-choice answer earns its full mark · two right picks of three earn two thirds · a wrong pick cancels a right one tip score.js on the MR keeps all 3 promises; read it knowing what it must do
Related lessons
- Read first: Entry-point-first reading
- Read first: Merge vs rebase on the commit graph
- Read first: Test pyramid and testing trophy
- Related: Severity-tagged review comments
- Related: Defect taxonomy scan
- Related: Test-gap analysis
- Related: Live walkthrough and improving the MR
- Related: Fix the class, not the instance
- Related: Owning every line, out loud
Checked against the tools and docs as of 2026-10-15.