Git & GitHub Module 3 – Exploring history
Find when a bug appeared: blame, log -S and bisect
Trace a line with git blame -w, -C and --ignore-rev, find when text appeared with git log -S and -G, and let git bisect run find the first bad commit.
What you will learn
- Trace a line's origin with git blame while ignoring whitespace, moved code and formatting commits
- Search history for added or removed strings with git log -S and -G, and follow a function with git log -L
- Find the first bad commit with git bisect, by hand and with git bisect run
- Handle untestable commits with git bisect skip and exit status 125, and use terms such as old and new
Before you start
What you’ll learn · Before you start
What you will learn
- Trace a line's origin with git blame while ignoring whitespace, moved code and formatting commits
- Search history for added or removed strings with git log -S and -G, and follow a function with git log -L
- Find the first bad commit with git bisect, by hand and with git bisect run
- Handle untestable commits with git bisect skip and exit status 125, and use terms such as old and new
Before you start
On this page
A bug report usually leads to three questions, and Git has a command for each. Who last changed this line?
git blame prints, beside every line of a file, the commit and the author that last touched it. When did this
text appear or disappear? git log -S lists the commits that added or removed a string, and git log -L follows
one function through its history. Which commit broke this behaviour? git bisect searches between a commit you
know is good and one you know is bad. Git checks out the commit in the middle, you test it, and every answer halves
what is left, so about 10 tests cover 1,000 commits. With a test script, git bisect run does the whole search
alone.
This lesson runs all three on a library with 200 commits, one of which breaks it. It also shows where each tool
points at the wrong commit: a reformatting commit that hides the real authors from git blame, and a test that
cannot tell a broken commit from an untestable one, which sends git bisect to the wrong answer.
A number formatter with one bad commit
inr.mjs is a small JavaScript library that writes numbers with Indian digit grouping: the last three digits, then
groups of two, so 1234567 becomes 12,34,567 and one hundred thousand becomes 1,00,000. Four people work on it. Asha
started it; Ravi added a helper in a separate file, digits.mjs; Meera added two constants and shortForm(); Imran
made negative numbers work. Then Meera moved Ravi’s helper into inr.mjs, Ravi wrote the constants with digit
separators, and Asha reformatted the whole file. The 20th commit is tagged v1.0. Most of the 200 commits only add
an example to the documentation, and the 137th breaks the grouping.
India-specific
The library’s constants follow Indian usage: 1,00,000 is one lakh and 1,00,00,000 is one crore, so LAKH is
100_000, CRORE was 10_000_000, and shortForm(2500000) returns "25 lakh".
// Builds the repository the examples of this lesson investigate: inr.mjs, a small library that writes numbers with
// Indian digit grouping (1,00,000 for one hundred thousand), with 200 commits by four people. One of the commits
// breaks the grouping; the examples find it. Names, emails and dates are fixed (India time, +05:30), so the commit
// IDs come out the same on every computer: run any example with Node.js 24 and you get the IDs shown on the page.
//
// The 200 commits are written by `git fast-import`, the command repository converters use, in one go: making them
// one `git commit` at a time would take far longer. The history is the same either way.
import { spawnSync } from 'node:child_process';
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
export const REPO = 'formatter';
const PEOPLE = {
Asha: 'Asha Rao <asha@example.com>',
Ravi: 'Ravi Kumar <ravi@example.com>',
Meera: 'Meera Iyer <meera@example.com>',
Imran: 'Imran Sheikh <imran@example.com>',
};
// Git reads no configuration of this computer, so the output does not depend on anyone's settings.
export const GIT_ENV = { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_TERMINAL_PROMPT: '0' };
// The git program in Git's own folder (`git --exec-path`): it skips the launcher that some systems put in front of
// git (macOS's /usr/bin/git), which makes the commands of a bisect session much faster. Elsewhere: plain `git`.
export const GIT = (() => {
const dir = spawnSync('git', ['--exec-path'], { encoding: 'utf8' }).stdout?.trim();
return dir && existsSync(join(dir, 'git')) ? join(dir, 'git') : 'git';
})();
function run(args, input) {
const r = spawnSync(GIT, args, { cwd: REPO, env: GIT_ENV, encoding: 'utf8', input });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed:\n${r.stderr}`);
return r.stdout;
}
// ---- the versions of inr.mjs -----------------------------------------------------------------------------------
const V1 = `// Numbers written with Indian digit grouping: 12,34,567 rather than 1,234,567.
export function groupIndian(n) {
const digits = String(n);
if (digits.length <= 3) return digits;
const rest = digits.slice(0, -3);
const last = digits.slice(-3);
return rest.replace(/\\B(?=(\\d{2})+$)/g, ',') + ',' + last;
}
`;
const DIGITS = `// Splits a string of digits before its last three digits.
export function splitLastThree(digits) {
return [digits.slice(0, -3), digits.slice(-3)];
}
`;
const V2 = `// Numbers written with Indian digit grouping: 12,34,567 rather than 1,234,567.
import { splitLastThree } from './digits.mjs';
export function groupIndian(n) {
const digits = String(n);
if (digits.length <= 3) return digits;
const [rest, last] = splitLastThree(digits);
return rest.replace(/\\B(?=(\\d{2})+$)/g, ',') + ',' + last;
}
`;
const SHORT = (lakh, crore) => `
export const LAKH = ${lakh};
export const CRORE = ${crore};
// 2500000 is "25 lakh" and 15000000 is "1.5 crore".
export function shortForm(n) {
if (n >= CRORE) return \`\${n / CRORE} crore\`;
if (n >= LAKH) return \`\${n / LAKH} lakh\`;
return groupIndian(n);
}
`;
const V3 = V2 + SHORT('100000', '10000000');
const NEGATIVE = (s) => s.replace("export function groupIndian(n) {\n", "export function groupIndian(n) {\n if (n < 0) return '-' + groupIndian(-n);\n");
const V4 = NEGATIVE(V3);
// splitLastThree moves from digits.mjs into inr.mjs, word for word.
const V5 = V4.replace("import { splitLastThree } from './digits.mjs';\n", '').replace('\nexport function groupIndian', `\n${DIGITS}\nexport function groupIndian`);
const V6 = V5.replace('= 100000;', '= 100_000;').replace('= 10000000;', '= 10_000_000;');
// A formatting-only commit: four spaces instead of two, double quotes instead of single ones.
const reformat = (s) => s.replace(/^( +)/gm, (m) => ' '.repeat(m.length * 2)).replace(/'/g, '"');
const V7 = reformat(V6);
// Later: rupees() for amounts with paise, CRORE dropped, and the commit that breaks the grouping.
const RUPEES = `
// 1234567.5 is "₹12,34,567.50".
export function rupees(amount) {
const [whole, paise] = Math.abs(amount).toFixed(2).split(".");
return (amount < 0 ? "-" : "") + "₹" + groupIndian(Number(whole)) + "." + paise;
}
`;
const V8 = V7 + RUPEES;
const V9 = V8.replace('export const CRORE = 10_000_000;\n', '').replace(/CRORE/g, '(100 * LAKH)');
const BROKEN = V9.replace(
` const digits = String(n);
if (digits.length <= 3) return digits;
const [rest, last] = splitLastThree(digits);
return rest.replace(/\\B(?=(\\d{2})+$)/g, ",") + "," + last;`,
` return String(n).replace(/\\B(?=(\\d{3})+$)/g, ",");`,
);
const V10 = BROKEN.replace('// 2500000 is', '// shortForm(2500000) is');
// ---- the history -----------------------------------------------------------------------------------------------
/** A small random number generator with a fixed seed, so the history is the same every time. */
function random(seed) {
let s = seed >>> 0;
return () => {
s = (s + 0x6d2b79f5) >>> 0;
let t = s;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/**
* Creates ./formatter from nothing: 200 commits on main, v1.0 tagged at the 20th. The commit that breaks the
* grouping is the 137th. With `brokenRefactor`, seven more commits (after the 108th) split inr.mjs
* into files halfway, so inr.mjs cannot even be loaded in those commits.
*/
export function buildFormatter({ brokenRefactor = false } = {}) {
rmSync(REPO, { recursive: true, force: true });
mkdirSync(REPO);
run(['init', '--quiet', '--initial-branch=main']);
const next = random(2026);
const stream = [];
let mark = 0;
let time = Date.parse('2026-01-05T09:30:00+05:30') / 1000;
const data = (text) => `data ${Buffer.byteLength(text)}\n${text}`;
const commit = (who, message, changes) => {
mark += 1;
if (mark > 1) time += 60 * (120 + Math.floor(next() * 1080));
stream.push('commit refs/heads/main', `mark :${mark}`, `author ${PEOPLE[who]} ${time} +0530`, `committer ${PEOPLE[who]} ${time} +0530`, data(`${message}\n`));
if (mark > 1) stream.push(`from :${mark - 1}`);
for (const [path, text] of Object.entries(changes)) stream.push(...(text === null ? [`D ${path}`] : [`M 100644 inline ${path}`, data(text)]));
stream.push('');
};
const examples = ['# Examples', ''];
const people = ['Asha', 'Ravi', 'Meera', 'Imran'];
const someone = () => people[Math.floor(next() * people.length)];
/** A commit that adds one line to docs/examples.md, as most commits of this project do. */
const docs = () => {
const n = Math.floor(next() * 9_000_000) + 1_000;
const grouped = String(n).slice(0, -3).replace(/\B(?=(\d{2})+$)/g, ',') + ',' + String(n).slice(-3);
examples.push(`- \`groupIndian(${n})\` returns \`${grouped}\``);
commit(someone(), `Add the example ${grouped} to the docs`, { 'docs/examples.md': `${examples.join('\n')}\n` });
};
commit('Asha', 'Start the number formatter', { 'inr.mjs': V1, 'README.md': '# inr\n\nWrites numbers with Indian digit grouping.\n' });
commit('Ravi', 'Add a helper that splits off the last three digits', { 'digits.mjs': DIGITS, 'inr.mjs': V2 });
commit('Meera', 'Add LAKH, CRORE and shortForm()', { 'inr.mjs': V3 });
commit('Imran', 'Handle negative numbers', { 'inr.mjs': V4 });
commit('Meera', 'Move splitLastThree into inr.mjs', { 'digits.mjs': null, 'inr.mjs': V5 });
commit('Ravi', 'Write the constants with digit separators', { 'inr.mjs': V6 });
commit('Asha', 'Reformat inr.mjs: four-space indent, double quotes', { 'inr.mjs': V7 });
while (mark < 20) docs();
stream.push('tag v1.0', `from :${mark}`, `tagger ${PEOPLE.Asha} ${time + 600} +0530`, data('First release\n'));
while (mark < 40) docs();
commit('Imran', 'Add rupees() for amounts with paise', { 'inr.mjs': V8 });
while (mark < 90) docs();
commit('Meera', 'Derive crore from lakh and drop the CRORE constant', { 'inr.mjs': V9 });
while (mark < 108) docs();
if (brokenRefactor) {
// The refactor imports files that do not exist yet; inr.mjs loads again only once the series is complete.
const steps = ['Start splitting inr.mjs: import grouping.mjs', 'Split inr.mjs: add short.mjs import', 'Split inr.mjs: move rupees() out', 'Split inr.mjs: rename the helpers', 'Split inr.mjs: update the README', 'Split inr.mjs: fix the imports', 'Undo the split of inr.mjs'];
steps.forEach((message, i) => commit('Ravi', message, { 'inr.mjs': i < steps.length - 1 ? `import { groupIndian } from './grouping.mjs';\n${V9}` : V9 }));
}
while (mark < 136 + (brokenRefactor ? 7 : 0)) docs();
commit('Ravi', 'Simplify groupIndian with one regular expression', { 'inr.mjs': BROKEN });
while (mark < 160 + (brokenRefactor ? 7 : 0)) docs();
commit('Asha', 'Show the call in the shortForm comment', { 'inr.mjs': V10 });
while (mark < 200 + (brokenRefactor ? 7 : 0)) docs();
run(['fast-import', '--quiet'], `${stream.join('\n')}\n`);
run(['reset', '--quiet', '--hard']);
}
/** What a git command prints, without showing it: a commit ID the next command needs, for example. */
export function gitOutput(...args) {
return run(args).trim();
}
/**
* Prints a command the way a terminal shows it, runs it in the repository and prints what it printed. Quotes work as
* in a shell: show(`git log --format='%h %s'`). `node` runs this example's Node.js. With `only`, a regular
* expression, only the lines that match are printed, followed by how many were left out.
*/
export function show(command, { only } = {}) {
const args = words(command);
const program = args.shift();
const bin = program === 'git' ? GIT : program === 'node' ? process.execPath : null;
if (!bin) throw new Error(`not a git or node command: ${command}`);
const { status, text: out } = capture(bin, args);
console.log(`$ ${command}`);
if (!only) {
if (out) console.log(out);
} else {
const lines = out.split('\n').filter((line) => line.trim() !== '');
const shown = lines.filter((line) => only.test(line));
if (shown.length > 0) console.log(shown.join('\n'));
const left = lines.length - shown.length;
console.log(`(${left} more ${left === 1 ? 'line' : 'lines'} not shown)`);
}
if (status !== 0) console.log(`(exit status ${status})`);
console.log('');
return status;
}
/** Runs a program with its output and its error messages going to one file, so they stay in the order written. */
function capture(bin, args) {
const file = 'run-output.tmp';
const fd = openSync(file, 'w');
try {
const r = spawnSync(bin, args, { cwd: REPO, env: GIT_ENV, stdio: ['ignore', fd, fd] });
return { status: r.status, text: readFileSync(file, 'utf8').replace(/\s+$/, '') };
} finally {
closeSync(fd);
rmSync(file, { force: true });
}
}
/** Splits a command into words the way a shell does with quotes: --format='%h %s' is one word, --format=%h %s. */
function words(command) {
const out = [];
let word = null;
for (let i = 0; i < command.length; i++) {
const c = command[i];
if (c === "'" || c === '"') {
const end = command.indexOf(c, i + 1);
word = (word ?? '') + command.slice(i + 1, end);
i = end;
} else if (/\s/.test(c)) {
if (word !== null) out.push(word);
word = null;
} else {
word = (word ?? '') + c;
}
}
if (word !== null) out.push(word);
return out;
} Every example builds this repository from nothing, runs commands in it and prints each command after a $ sign,
followed by the output, as a terminal shows it. The names, email addresses and dates are fixed, in India Standard
Time, so the commit IDs match on any computer: put formatter.mjs and an example in an empty folder and run it with
node. When a command fails, the example prints its exit status after the output.
git blame: who last changed each line
// git blame on groupIndian() as it was at the v1.0 release: first plain, then ignoring changes to white space.
// Run it with Node.js 24 in a folder that holds formatter.mjs: node blame.mjs
import { buildFormatter, show } from './formatter.mjs';
buildFormatter();
show('git blame -L :groupIndian v1.0 -- inr.mjs');
show('git blame -w -L :groupIndian v1.0 -- inr.mjs');
show('git last-modified'); Output
$ git blame -L :groupIndian v1.0 -- inr.mjs
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 8) export function groupIndian(n) {
4aa918d2 (Asha Rao 2026-01-07 16:58:00 +0530 9) if (n < 0) return "-" + groupIndian(-n);
4aa918d2 (Asha Rao 2026-01-07 16:58:00 +0530 10) const digits = String(n);
4aa918d2 (Asha Rao 2026-01-07 16:58:00 +0530 11) if (digits.length <= 3) return digits;
4aa918d2 (Asha Rao 2026-01-07 16:58:00 +0530 12) const [rest, last] = splitLastThree(digits);
4aa918d2 (Asha Rao 2026-01-07 16:58:00 +0530 13) return rest.replace(/\B(?=(\d{2})+$)/g, ",") + "," + last;
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 14) }
05f897c1 (Meera Iyer 2026-01-06 03:14:00 +0530 15)
$ git blame -w -L :groupIndian v1.0 -- inr.mjs
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 8) export function groupIndian(n) {
4aa918d2 (Asha Rao 2026-01-07 16:58:00 +0530 9) if (n < 0) return "-" + groupIndian(-n);
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 10) const digits = String(n);
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 11) if (digits.length <= 3) return digits;
99418e9b (Ravi Kumar 2026-01-05 19:41:00 +0530 12) const [rest, last] = splitLastThree(digits);
4aa918d2 (Asha Rao 2026-01-07 16:58:00 +0530 13) return rest.replace(/\B(?=(\d{2})+$)/g, ",") + "," + last;
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 14) }
05f897c1 (Meera Iyer 2026-01-06 03:14:00 +0530 15)
$ git last-modified
c817ecc98421e85b6e76340daabedee33735d29f docs
da69a8677d1e3454e66485b8f2b4b12d5a5e5c57 inr.mjs
bb0c56a7cc1606b7090ec9d4c38c2ae8cf88612c README.md
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node blame.mjs
Each line of git blame output starts with the commit that last changed the line, then the author, the date, the
line number and the line itself. -L :groupIndian keeps the lines of one function, found the way git diff finds
the headings of its hunks. v1.0 -- asks for the file as it was at that release. A commit ID that starts with ^
belongs to a boundary: here the repository’s first commit, which git blame treats as a boundary unless you add
--root. The other IDs are printed one character longer, so that they line up with the marked ones; since Git 2.56,
that extra character appears only when some line carries a mark.
The first command shows the problem every long-lived project meets. Asha’s reformatting commit, 4aa918d, changed
the indentation of almost every line, so it owns five of them, including the check for negative numbers that Imran
wrote. -w ignores changes in white space while Git works out where each line came from, and three lines go
back to their real authors: Asha’s first version and Ravi’s helper call. Two lines stay with the reformatting
commit, because it also changed their quotes from single to double.
The last command is the file-level version of the same question. git last-modified, added in Git 2.52 and still
marked experimental, prints the last commit that changed each file and folder.
Moved code: -M and -C
// A function that moved from digits.mjs into inr.mjs: who wrote it, with and without -C.
import { buildFormatter, show } from './formatter.mjs';
buildFormatter();
show('git blame -w -L :splitLastThree v1.0 -- inr.mjs');
show('git blame -w -C -L :splitLastThree v1.0 -- inr.mjs'); Output
$ git blame -w -L :splitLastThree v1.0 -- inr.mjs
20d043a9 (Meera Iyer 2026-01-07 06:15:00 +0530 4) export function splitLastThree(digits) {
20d043a9 (Meera Iyer 2026-01-07 06:15:00 +0530 5) return [digits.slice(0, -3), digits.slice(-3)];
20d043a9 (Meera Iyer 2026-01-07 06:15:00 +0530 6) }
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 7)
$ git blame -w -C -L :splitLastThree v1.0 -- inr.mjs
99418e9b digits.mjs (Ravi Kumar 2026-01-05 19:41:00 +0530 4) export function splitLastThree(digits) {
99418e9b digits.mjs (Ravi Kumar 2026-01-05 19:41:00 +0530 5) return [digits.slice(0, -3), digits.slice(-3)];
99418e9b digits.mjs (Ravi Kumar 2026-01-05 19:41:00 +0530 6) }
^bb0c56a inr.mjs (Asha Rao 2026-01-05 09:30:00 +0530 7)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node blame_moves.mjs
Without help, git blame gives Meera’s commit, the one that moved splitLastThree() from digits.mjs into
inr.mjs, as the origin of those three lines, although Ravi wrote them. -C looks for lines that were moved or
copied from other files changed in the same commit, finds them in digits.mjs, prints that file’s name in a new
column and gives Ravi’s commit. Given twice, -C -C also looks for copies from other files in the commit that
created the file; three times, it looks in every commit, which is slowest. -M does the same for lines moved
within one file, and -C includes it.
--ignore-rev: commits that only reformat
// Leaving the formatting-only commit out of git blame: once on the command line, then for good with a file.
import { writeFileSync } from 'node:fs';
import { buildFormatter, gitOutput, show } from './formatter.mjs';
buildFormatter();
const reformat = gitOutput('log', '-1', '--format=%H', '--grep=Reformat');
show(`git blame --ignore-rev ${reformat.slice(0, 7)} -L :groupIndian v1.0 -- inr.mjs`);
// The usual setup: the commits to ignore, in a file of the repository, named in the configuration.
writeFileSync('formatter/.git-blame-ignore-revs', `# Reformat inr.mjs: four-space indent, double quotes\n${reformat}\n`);
show('git config blame.ignoreRevsFile .git-blame-ignore-revs');
show('git config blame.markIgnoredLines true');
show('git blame -L :groupIndian v1.0 -- inr.mjs'); Output
$ git blame --ignore-rev 4aa918d -L :groupIndian v1.0 -- inr.mjs
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 8) export function groupIndian(n) {
37a7facf (Imran Sheikh 2026-01-06 17:08:00 +0530 9) if (n < 0) return "-" + groupIndian(-n);
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 10) const digits = String(n);
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 11) if (digits.length <= 3) return digits;
99418e9b (Ravi Kumar 2026-01-05 19:41:00 +0530 12) const [rest, last] = splitLastThree(digits);
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 13) return rest.replace(/\B(?=(\d{2})+$)/g, ",") + "," + last;
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 14) }
05f897c1 (Meera Iyer 2026-01-06 03:14:00 +0530 15)
$ git config blame.ignoreRevsFile .git-blame-ignore-revs
$ git config blame.markIgnoredLines true
$ git blame -L :groupIndian v1.0 -- inr.mjs
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 8) export function groupIndian(n) {
?37a7fac (Imran Sheikh 2026-01-06 17:08:00 +0530 9) if (n < 0) return "-" + groupIndian(-n);
^?bb0c56 (Asha Rao 2026-01-05 09:30:00 +0530 10) const digits = String(n);
^?bb0c56 (Asha Rao 2026-01-05 09:30:00 +0530 11) if (digits.length <= 3) return digits;
?99418e9 (Ravi Kumar 2026-01-05 19:41:00 +0530 12) const [rest, last] = splitLastThree(digits);
^?bb0c56 (Asha Rao 2026-01-05 09:30:00 +0530 13) return rest.replace(/\B(?=(\d{2})+$)/g, ",") + "," + last;
^bb0c56a (Asha Rao 2026-01-05 09:30:00 +0530 14) }
05f897c1 (Meera Iyer 2026-01-06 03:14:00 +0530 15)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node blame_ignore.mjs
--ignore-rev <commit> blames the file as if that commit never happened: every line it changed goes back to
the commit that changed the line before it. Imran’s line returns to him, and the two lines with new quotes return to
Asha’s first version. Where Git cannot find an earlier owner, for a line that the ignored commit added, the line
stays with the ignored commit.
Typing the option every time does not last, so projects list such commits in a file, one full commit ID per line
with # comments, usually named .git-blame-ignore-revs and committed to the repository. Each person points Git at
it once with git config blame.ignoreRevsFile .git-blame-ignore-revs, and every git blame after that skips the
listed commits. The example also turns on blame.markIgnoredLines, which marks with ? each line that was moved to
another commit because of an ignored one; blame.markUnblamableLines marks with * the lines that could not be
moved. GitHub’s blame view reads a .git-blame-ignore-revs file in the root of the repository by itself.
The pickaxe: git log -S and -G
// When did the CRORE constant appear and disappear? The pickaxe (-S), its regular-expression cousin (-G), and
// git grep for where the text is now and where it was at the v1.0 release.
import { buildFormatter, show } from './formatter.mjs';
buildFormatter();
show('git log -S CRORE --oneline');
show('git log -G CRORE --oneline');
show('git grep -n CRORE');
show('git grep -n CRORE v1.0'); Output
$ git log -S CRORE --oneline
f719af2 Derive crore from lakh and drop the CRORE constant
05f897c Add LAKH, CRORE and shortForm()
$ git log -G CRORE --oneline
f719af2 Derive crore from lakh and drop the CRORE constant
4aa918d Reformat inr.mjs: four-space indent, double quotes
b403596 Write the constants with digit separators
05f897c Add LAKH, CRORE and shortForm()
$ git grep -n CRORE
(exit status 1)
$ git grep -n CRORE v1.0
v1.0:inr.mjs:17:export const CRORE = 10_000_000;
v1.0:inr.mjs:21: if (n >= CRORE) return `${n / CRORE} crore`;
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node pickaxe.mjs
git log -S CRORE lists the commits that change how many times CRORE occurs: the commit that added the
constant and the one that removed it. That is usually the question: when did this come in, and when did it go?
git log -G CRORE lists the commits whose diff adds or removes a line that matches a regular expression, so it
also finds the two commits that edited lines containing CRORE without changing how many there were: Ravi’s digit
separators and Asha’s reformatting. Use -S to date an appearance or a removal and -G to see every change that
touched the text. -S takes a plain string; add --pickaxe-regex to give it a regular expression instead.
Both search only HEAD’s history unless you add --all, take -- <path> to stay in one file, and combine with -p
to print each change. git grep searches the files themselves: in your working tree it finds no CRORE, which
is why it exits with status 1, and given a revision, git grep -n CRORE v1.0, it searches the files as they were at
that release.
One function’s history: git log -L
// The history of one function, groupIndian(): the commits that changed it, then the latest change in full.
import { buildFormatter, show } from './formatter.mjs';
buildFormatter();
show('git log --oneline --no-patch -L :groupIndian:inr.mjs');
show('git log -1 -L :groupIndian:inr.mjs'); Output
$ git log --oneline --no-patch -L :groupIndian:inr.mjs
fb6c498 Simplify groupIndian with one regular expression
4aa918d Reformat inr.mjs: four-space indent, double quotes
37a7fac Handle negative numbers
05f897c Add LAKH, CRORE and shortForm()
99418e9 Add a helper that splits off the last three digits
bb0c56a Start the number formatter
$ git log -1 -L :groupIndian:inr.mjs
commit fb6c498bc0cbf4fa83a5f95c402d9ed300011f59
Author: Ravi Kumar <ravi@example.com>
Date: Fri Mar 6 12:58:00 2026 +0530
Simplify groupIndian with one regular expression
diff --git a/inr.mjs b/inr.mjs
index d2c336c..21457dd 100644
--- a/inr.mjs
+++ b/inr.mjs
@@ -8,8 +8,5 @@ export function splitLastThree(digits) {
export function groupIndian(n) {
if (n < 0) return "-" + groupIndian(-n);
- const digits = String(n);
- if (digits.length <= 3) return digits;
- const [rest, last] = splitLastThree(digits);
- return rest.replace(/\B(?=(\d{2})+$)/g, ",") + "," + last;
+ return String(n).replace(/\B(?=(\d{3})+$)/g, ",");
}
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node follow_function.mjs
git log -L :groupIndian:inr.mjs follows the lines of one function back through history, even as other changes
move it up or down the file. The range runs from the first line that matches the name to the next line that looks
like the start of a definition, which for a file type Git has no rules for means a line that begins with a letter,
_ or $. The blank line Meera added after the function falls inside that range, which is why her commit is
listed. -L prints each change as a patch; --no-patch leaves only the list. For many languages, Git has better
rules for where a function starts, chosen in .gitattributes, for example *.py diff=python.
The second command shows the newest change to the function in full: the commit that replaced the two-step grouping
with one regular expression for groups of three. Since Git 2.55, -L also works with the pickaxe options and
--diff-filter, and with --name-only and --raw.
git bisect: a binary search through history
git blame and the pickaxe need you to know which line or text to ask about. When all you know is that something
worked once and fails now, git bisect finds the first commit that fails. You give it one bad commit and one or
more good ones. Git checks out a commit halfway between them, you test it and say git bisect good or
git bisect bad, and Git halves the range again until one commit is left.
| Commits to search | Tests needed |
|---|---|
| 10 | 4 |
| 100 | 7 |
| 1,000 | 10 |
| 10,000 | 14 |
| 100,000 | 17 |
Each test halves the suspects, so n commits take about log₂ n tests, rounded up. The test is what costs time: if a build and a test run take five minutes, 10 tests over 1,000 commits take 50 minutes, while testing every commit one by one would take five thousand minutes, about three and a half days.
The test here is check.mjs, three lines of code: it loads inr.mjs from the commit that is checked out and exits
with status 0 if one hundred thousand comes out as 1,00,000, or 1 if not.
// The test git bisect runs in each commit: exit status 0 if one lakh is written 1,00,000 (good), 1 if not (bad).
import { pathToFileURL } from 'node:url';
const { groupIndian } = await import(pathToFileURL('inr.mjs').href);
process.exit(groupIndian(100000) === '1,00,000' ? 0 : 1); // The first two steps of a bisect session done by hand: mark the ends, test the commit Git checks out, say what
// the test found, and stop. check.mjs is the test (exit status 0: good, 1: bad).
import { buildFormatter, show } from './formatter.mjs';
buildFormatter();
show('git --version');
// Check both ends first: the newest commit fails the test, the v1.0 release passes it.
show('node ../check.mjs');
show('git switch --quiet --detach v1.0');
show('node ../check.mjs');
show('git switch --quiet main');
show('git bisect start');
show('git bisect bad');
show('git bisect good v1.0');
show('node ../check.mjs');
show('git bisect good');
show('node ../check.mjs');
show('git bisect bad');
show('git bisect reset'); Output
$ git --version git version 2.54.0 (Apple Git-157) $ node ../check.mjs (exit status 1) $ git switch --quiet --detach v1.0 $ node ../check.mjs $ git switch --quiet main $ git bisect start status: waiting for both good and bad commits $ git bisect bad status: waiting for good commit(s), bad commit known $ git bisect good v1.0 Bisecting: 89 revisions left to test after this (roughly 7 steps) [038041d3d9315de22703c008e331e9c0cce0f54b] Add the example 33,70,124 to the docs $ node ../check.mjs $ git bisect good Bisecting: 44 revisions left to test after this (roughly 6 steps) [99a61afa46a1dbbe0fa23f1e72a0de8cf58777e8] Add the example 11,75,398 to the docs $ node ../check.mjs (exit status 1) $ git bisect bad Bisecting: 22 revisions left to test after this (roughly 5 steps) [c5aa61dada8b73c4b18fbe7fe0e5cbf4d8883fbb] Add the example 65,41,668 to the docs $ git bisect reset Previous HEAD position was c5aa61d Add the example 65,41,668 to the docs Switched to branch 'main'
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node bisect_by_hand.mjs
The session starts by checking both ends: the newest commit fails the test and v1.0 passes it, and a test that
passes prints nothing. Then git bisect start opens a session, git bisect bad marks the current commit and
git bisect good v1.0 marks the release. Git now knows the bug lies in the 180 commits after v1.0, checks out the
110th and says how much is left after this test: 89 revisions, roughly 7 more steps. Each answer narrows the range,
and each time Git checks out the next commit to test. git bisect reset ends the session and puts you back on the
branch you started from. Leave it out and you stay on a detached commit in the middle of history.
When you mark a commit wrongly, git bisect log prints the session’s commands; save them to a file, delete the
wrong line, then run git bisect reset and git bisect replay on the file. git bisect visualize, or
git bisect view, shows the commits still under suspicion.
Let Git run the test: git bisect run
Marking commits by hand works, but a test that answers with its exit status lets Git do every step:
// The whole search done by Git: git bisect run calls check.mjs in every commit it checks out.
import { buildFormatter, show } from './formatter.mjs';
buildFormatter();
show('git bisect start HEAD v1.0');
show('git bisect run node ../check.mjs');
show('git rev-parse --short refs/bisect/bad');
show('git bisect reset'); Output
$ git bisect start HEAD v1.0
Bisecting: 89 revisions left to test after this (roughly 7 steps)
[038041d3d9315de22703c008e331e9c0cce0f54b] Add the example 33,70,124 to the docs
$ git bisect run node ../check.mjs
running 'node' '../check.mjs'
Bisecting: 44 revisions left to test after this (roughly 6 steps)
[99a61afa46a1dbbe0fa23f1e72a0de8cf58777e8] Add the example 11,75,398 to the docs
running 'node' '../check.mjs'
Bisecting: 22 revisions left to test after this (roughly 5 steps)
[c5aa61dada8b73c4b18fbe7fe0e5cbf4d8883fbb] Add the example 65,41,668 to the docs
running 'node' '../check.mjs'
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[d4e755f49346404ef563140f6faacfb6213a2bbd] Add the example 10,30,482 to the docs
running 'node' '../check.mjs'
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[fb6c498bc0cbf4fa83a5f95c402d9ed300011f59] Simplify groupIndian with one regular expression
running 'node' '../check.mjs'
Bisecting: 2 revisions left to test after this (roughly 1 step)
[2524d0f5f6ede4777bcbd3d500f504b5c1623205] Add the example 13,33,763 to the docs
running 'node' '../check.mjs'
Bisecting: 0 revisions left to test after this (roughly 1 step)
[70c95102205c43619c690debb3e0e29754bcb0eb] Add the example 27,79,463 to the docs
running 'node' '../check.mjs'
fb6c498bc0cbf4fa83a5f95c402d9ed300011f59 is the first bad commit
commit fb6c498bc0cbf4fa83a5f95c402d9ed300011f59
Author: Ravi Kumar <ravi@example.com>
Date: Fri Mar 6 12:58:00 2026 +0530
Simplify groupIndian with one regular expression
inr.mjs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
bisect found first bad commit
$ git rev-parse --short refs/bisect/bad
fb6c498
$ git bisect reset
Previous HEAD position was 70c9510 Add the example 27,79,463 to the docs
Switched to branch 'main'
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node bisect_run.mjs
git bisect start HEAD v1.0 marks both ends in one command: the bad commit first, then any good ones.
git bisect run node ../check.mjs runs the test in each commit Git checks out and reads its exit status:
| Exit status of the test | What Git does with the commit |
|---|---|
| 0 | marks it good |
| 1 to 127, except 125 | marks it bad |
| 125 | skips it: this commit cannot be tested |
| 128 to 255 | stops the whole session |
Seven tests found the commit, 137th of 200: “Simplify groupIndian with one regular expression”. The diagram follows the search from the run above.
Seven tests for 180 suspects, from the recorded run
Text description of the diagram
The commits of the formatter repository are numbered 1 to 200, oldest first. The diagram is a column of boxes, read from top to bottom.
- At the start, commit 20 (the v1.0 tag) is known to be good and commit 200 (HEAD) is known to be bad, so the first bad commit is one of the 180 commits from 21 to 200.
- Test 1: commit 110 is good, which leaves 111 to 200, 90 commits.
- Test 2: commit 155 is bad, which leaves 111 to 155, 45 commits.
- Test 3: commit 132 is good, which leaves 133 to 155, 23 commits.
- Test 4: commit 143 is bad, which leaves 133 to 143, 11 commits.
- Test 5: commit 137 is bad, which leaves 133 to 137, 5 commits.
- Test 6: commit 134 is good, which leaves 135 to 137, 3 commits.
- Test 7: commit 136 is good, which leaves commit 137 alone.
The last box names commit 137, fb6c498, "Simplify groupIndian with one regular expression", as the first bad commit. Every test halved the number of suspects, so 180 suspects took 7 tests.
refs/bisect/bad holds the answer until the session is reset, which is the way for a script to read it. The wording
of the result line is not stable: Git 2.54 printed is the first bad commit, and Git 2.55 and later quote the term,
is the first 'bad' commit.
A test that cannot even be started returns 126 or 127, the statuses a shell uses for a command it could not run or
find. Git counts those as bad, but the first time it sees one in a session it reruns the command on a commit already
marked good. If that fails the same way, Git stops with
bogus exit code 127 for good revision rather than search with a broken script. No such check protects you from a test that crashes for another
reason, as the next section shows.
When the test cannot tell: skip, and the commit bisect got wrong
In real histories some commits cannot be tested at all: a refactor pushed as a series in which the code does not
load until the last commit, a commit that does not build, a test that needs a file added later. The example builds
the same library with seven such commits, a split of inr.mjs into several files that imports files that do not
exist yet, placed among the good commits before the bug.
// Like check.mjs, but a commit whose inr.mjs cannot be loaded at all is skipped (exit status 125), not called bad.
import { pathToFileURL } from 'node:url';
let inr;
try {
inr = await import(pathToFileURL('inr.mjs').href);
} catch {
process.exit(125);
}
process.exit(inr.groupIndian(100000) === '1,00,000' ? 0 : 1); // The same search in a history with seven commits whose inr.mjs cannot even be loaded: check.mjs calls them bad and
// Git names the wrong commit; check-skip.mjs skips them (exit status 125) and Git names the right one. Only the
// verdict of each run is printed (node prints a long error report for every commit it cannot load); git bisect log
// then lists every commit that was tested and what the test said.
import { buildFormatter, show } from './formatter.mjs';
buildFormatter({ brokenRefactor: true });
for (const test of ['../check.mjs', '../check-skip.mjs']) {
show('git bisect start HEAD v1.0');
show(`git bisect run node ${test}`, { only: /is the first '?bad'? commit/ });
show('git bisect log', { only: /^# (good|bad|skip|first bad commit):/ });
show('git bisect reset');
} Output
$ git bisect start HEAD v1.0 Bisecting: 93 revisions left to test after this (roughly 7 steps) [c71d7c8fa2cc693ee23d5cbfb23ae7f311581dea] Split inr.mjs: update the README $ git bisect run node ../check.mjs af696ed8d5b7f0e0d6eed29b55abe87c3c3b7b65 is the first bad commit (74 more lines not shown) $ git bisect log # bad: [b244922aede113689ba9311ac08dd17056b31730] Add the example 3,96,031 to the docs # good: [cccf16121616f9d8b9fd22624003dfccf31605e6] Add the example 56,63,673 to the docs # bad: [c71d7c8fa2cc693ee23d5cbfb23ae7f311581dea] Split inr.mjs: update the README # good: [2d0eca9044071b3944ec278a4d849a4a4fdd7ca9] Add the example 84,22,329 to the docs # good: [4b81f588c679711502aa248a208903cd71cc9b55] Add the example 83,96,336 to the docs # good: [d8ca130f5b4b7399a7e8c8dd0078167fbfa4989e] Add the example 45,47,502 to the docs # good: [40f014df405ffbf03214690d1b19b35ebb0dbe22] Add the example 14,00,166 to the docs # bad: [f9ac2ac9909aa8e12221e562c484d14033c08844] Split inr.mjs: add short.mjs import # bad: [af696ed8d5b7f0e0d6eed29b55abe87c3c3b7b65] Start splitting inr.mjs: import grouping.mjs # good: [e4c05815f506fa3dcc6f4e0a3db884e53c934f83] Add the example 79,83,391 to the docs # first bad commit: [af696ed8d5b7f0e0d6eed29b55abe87c3c3b7b65] Start splitting inr.mjs: import grouping.mjs (9 more lines not shown) $ git bisect reset Previous HEAD position was e4c0581 Add the example 79,83,391 to the docs Switched to branch 'main' $ git bisect start HEAD v1.0 Bisecting: 93 revisions left to test after this (roughly 7 steps) [c71d7c8fa2cc693ee23d5cbfb23ae7f311581dea] Split inr.mjs: update the README $ git bisect run node ../check-skip.mjs e4ec84e48c18146527d14bfb9f71d2e1c67fec98 is the first bad commit (38 more lines not shown) $ git bisect log # bad: [b244922aede113689ba9311ac08dd17056b31730] Add the example 3,96,031 to the docs # good: [cccf16121616f9d8b9fd22624003dfccf31605e6] Add the example 56,63,673 to the docs # skip: [c71d7c8fa2cc693ee23d5cbfb23ae7f311581dea] Split inr.mjs: update the README # skip: [51e0dbf6f0759f16bb1dcb8a659745f22f4fa30e] Split inr.mjs: fix the imports # skip: [31db887e146e41dfe8e4e00697905ac0f476761f] Split inr.mjs: move rupees() out # good: [df393f27221ae7d0be8262be73451ed7a966e9f9] Add the example 5,31,716 to the docs # good: [53ebe9357ca9e38f415f99a6ddcec6865f99fa14] Add the example 4,78,016 to the docs # bad: [ac0ad52d3a8f3546f13020af324d5713e1d7d9f6] Add the example 16,67,582 to the docs # bad: [23049e083ab29d72cac0d691d6b575ff61785cb4] Add the example 79,89,479 to the docs # bad: [85ba402b80d0721c0330e3e7b350c7ea6a0abac2] Add the example 35,06,974 to the docs # bad: [9df57aa0a15bda770ef0a7e3a3e73ae62125a165] Add the example 25,49,952 to the docs # good: [1d2d34045a49e9556e73380d7795b078890f91ea] Add the example 8,56,196 to the docs # bad: [e4ec84e48c18146527d14bfb9f71d2e1c67fec98] Simplify groupIndian with one regular expression # first bad commit: [e4ec84e48c18146527d14bfb9f71d2e1c67fec98] Simplify groupIndian with one regular expression (12 more lines not shown) $ git bisect reset Previous HEAD position was e4ec84e Simplify groupIndian with one regular expression Switched to branch 'main'
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node bisect_skip.mjs
With check.mjs, Git names the wrong commit. Node cannot load inr.mjs in the seven broken commits, prints a
SyntaxError and exits with status 1, which Git reads as bad. The first broken commit, af696ed “Start splitting
inr.mjs”, comes out as the first bad commit, although the grouping bug is 35 commits later. Nothing in the output
warns that the answer is wrong. The git bisect log lines show how it happened: the very first test landed on a
broken commit and was marked bad.
With check-skip.mjs, Git skips those commits and finds the real culprit, e4ec84e “Simplify groupIndian with
one regular expression”, after 11 tests, three of them spent on commits it had to skip. By hand the
same thing is git bisect skip, which also takes ranges, for example git bisect skip v2.5..v2.6. If a skipped
commit sits right next to the culprit, Git cannot choose between them and lists every candidate instead.
Two more habits keep the answer right. Make the test deterministic: one wrong answer from a flaky test sends the search into the wrong half, and Git trusts it completely, so run an unreliable test several times per commit or fix it first. And keep commits small, each one building and passing its tests, so that every commit can be tested and the commit bisect names is small enough to read.
Other words than good and bad, and merges
Bisect can look for any change, not only a bug. When you search for the commit that fixed something, the old
state is the broken one, and good and bad read backwards. Start the session without commits and use git bisect old
and git bisect new instead; or choose your own words, for example
git bisect start --term-old=broken --term-new=fixed, and then mark commits with git bisect broken and
git bisect fixed. git bisect terms prints the words of the current session. One session cannot mix good and bad
with old and new.
In a history with many merges, git bisect start --first-parent follows only the first parent of each merge,
so it tests the main line and names the merge that brought the change in. You can then search inside the merged
branch if you need the exact commit.
What newer Git versions change
Git 2.56 added an option that ends a bisect session by itself. The example below gives it to Git 2.54.0, the Git of these examples, to show what an older Git does with an option it does not know:
// What Git 2.54.0 does with --reset-when-found, an option that Git 2.56 added: git bisect start refuses it, and
// git bisect run takes it for the name of the command to run.
import { buildFormatter, show } from './formatter.mjs';
buildFormatter();
show('git --version');
show('git bisect start --reset-when-found HEAD v1.0');
show('git bisect start HEAD v1.0');
show('git bisect run --reset-when-found node ../check.mjs');
show('git bisect reset'); Output
$ git --version git version 2.54.0 (Apple Git-157) $ git bisect start --reset-when-found HEAD v1.0 error: unrecognized option: '--reset-when-found' (exit status 1) $ git bisect start HEAD v1.0 Bisecting: 89 revisions left to test after this (roughly 7 steps) [038041d3d9315de22703c008e331e9c0cce0f54b] Add the example 33,70,124 to the docs $ git bisect run --reset-when-found node ../check.mjs running '--reset-when-found' 'node' '../check.mjs' '--reset-when-found' 'node' '../check.mjs': --reset-when-found: command not found [cccf16121616f9d8b9fd22624003dfccf31605e6] Add the example 56,63,673 to the docs running '--reset-when-found' 'node' '../check.mjs' '--reset-when-found' 'node' '../check.mjs': --reset-when-found: command not found error: bogus exit code 127 for good revision [038041d3d9315de22703c008e331e9c0cce0f54b] Add the example 33,70,124 to the docs (exit status 1) $ git bisect reset Previous HEAD position was 038041d Add the example 33,70,124 to the docs Switched to branch 'main'
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node bisect_versions.mjs
Version note
The examples ran on Git 2.54.0. Git 2.56 added --reset-when-found[=original|found] to git bisect start and
git bisect run: once the first bad commit is found, Git reports it and ends the session itself, returning to the
commit you started from (original, the default) or staying on the culprit (found). Git 2.54.0 refuses the option
in git bisect start, and git bisect run takes it for the name of the test command, which the shell cannot find,
as the run above shows; without the option, finish with git bisect reset. Git 2.56 also makes git branch -d say
so when it cannot delete a branch because a bisect session is using it. For git blame, Git 2.52 lets a setting that
names a file, such as blame.ignoreRevsFile, be marked :(optional), so a repository without the file is no error,
and Git 2.53 added --diff-algorithm.
Which tool for which question
| What you have | What you ask | What to run |
|---|---|---|
| A line of code | Who changed it last, and in which commit? | git blame -w -C -L <range> <file>, with the formatting commits ignored |
| A piece of text | When was it added or removed? | git log -S'<text>', or git log -G'<regex>' for every change to it |
| A function | How has it changed over time? | git log -L :<name>:<file> |
| A test that passes on an old commit and fails now | Which commit broke it? | git bisect start <bad> <good>, then git bisect run <test> |
Practise: decide good, bad or skip
A git bisect run test needs three answers, not two. Write the decision for a second bug, in shortForm(); the
sample tests give it commits that work, commits that fail and commits that cannot be loaded.
Exercise · Easy · JavaScript
Decide good, bad or skip for git bisect run
A second bug report says shortForm() of inr.mjs went wrong somewhere in the last few hundred commits: it should return "25 lakh" for 2500000 and "1.5 crore" for 15000000. Write the decision that a git bisect run test makes in each commit it is given.
Write verdict(load) in verdict.mjs and export it. load() returns the inr.mjs module of the commit under test, an object with its exports, or throws when the module cannot be loaded. verdict returns the exit status the test should end with:
0(good) whenshortFormgives both answers right;1(bad) when it gives either answer wrong, or throws while it computes one: a crash in the code under test is a bug of that commit;125(skip) when the commit cannot be tested at all:load()throws, or the module has noshortFormfunction.
The sample tests call verdict with stand-in modules, so they run in your browser without Git.
Starter code · verdict.mjs
/**
* The exit status a git bisect run test ends with for one commit: 0 (good), 1 (bad) or 125 (skip).
* load() returns that commit's inr.mjs module, or throws when it cannot be loaded.
*/
export function verdict(load) {
// Replace this line with your code.
return 0;
} The sample tests · verdict.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { verdict } from './verdict.mjs';
// Stand-ins for inr.mjs in different commits.
const right = (n) => (n >= 10_000_000 ? `${n / 10_000_000} crore` : n >= 100_000 ? `${n / 100_000} lakh` : String(n));
const commitWith = (shortForm) => () => ({ shortForm });
test('both answers right: good (0)', () => {
assert.equal(verdict(commitWith(right)), 0);
});
test('the lakh answer wrong: bad (1)', () => {
assert.equal(verdict(commitWith((n) => (n === 2_500_000 ? '25,00,000' : right(n)))), 1);
});
test('only the crore answer wrong: bad (1)', () => {
assert.equal(verdict(commitWith((n) => (n === 15_000_000 ? '150 lakh' : right(n)))), 1);
});
test('shortForm throws: bad (1)', () => {
assert.equal(verdict(commitWith(() => { throw new TypeError('n.toFixed is not a function'); })), 1);
});
test('the module cannot be loaded: skip (125)', () => {
assert.equal(verdict(() => { throw new SyntaxError("Identifier 'groupIndian' has already been declared"); }), 125);
});
test('a module without shortForm: skip (125)', () => {
assert.equal(verdict(() => ({ groupIndian: right })), 125);
}); A hint
Work in the order the cases can happen: first try load() (a failure there means skip), then check that shortForm is a function, and only then call it, inside its own try, because an error raised by shortForm means bad, not skip.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
What is the difference between git log -S and git log -G?
Both search the changes in history, not the messages. -S<string> lists commits that change the number of times a
string occurs: the commit that added it and the one that removed it, so it answers “when did this appear or go?”.
-G<regex> lists commits whose diff adds or removes a line matching a regular expression, so it also catches edits
to lines that contain the text when the count stays the same, such as a reformat or a changed value. In a repository
where a constant was added, edited twice and removed, -S lists two commits and -G four. -S takes a plain string
unless you add --pickaxe-regex. Both search only the history of HEAD unless you add --all.
What does exit status 125 mean to git bisect run?
It means “skip”: the commit cannot be tested, for example because it does not build or its code cannot be loaded, so Git neither marks it good nor bad and picks a commit near it instead. Status 0 means good, 1 to 127 other than 125 mean bad, and 128 or more stop the session. A test should exit 125 only when it could not run the check at all; when the code runs and gives a wrong result, or crashes inside the code under test, the right status is bad. A test that lets an untestable commit look bad makes bisect report the wrong commit without any warning.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A page that rendered in 200 ms now takes 2 s, and the slowdown came in somewhere in the last 600 commits. How do you find the commit with git bisect, how many measurements will it take, and what can make the answer wrong?
-
Mid to senior
Your team ran a code formatter over the whole repository in one commit, and now git blame shows that commit on almost every line. How do you get useful blame back for everyone, on the command line and on GitHub, and what will it not fix?
Key takeaways
git blamenames the last commit that changed each line.-wignores white space,-Cfollows lines moved or copied from other files, and--ignore-revor a.git-blame-ignore-revsfile named inblame.ignoreRevsFiletakes formatting commits out of the answer.git log -S'<text>'finds the commits that added or removed text,-G'<regex>'every commit that changed a line containing it, andgit grepsearches the files now or at any revision.git log -L :<name>:<file>follows one function through history, as patches or, with--no-patch, as a list.git bisectneeds one bad and one good commit and halves the suspects with every test: about 10 tests for 1,000 commits. Always finish withgit bisect reset.- For
git bisect run, the test exits 0 for good, 1 for bad and 125 for a commit that cannot be tested. A crash that the test does not turn into 125 is read as bad and can send the search to the wrong commit. - Scripts read the result from
refs/bisect/bad, not from the message, whose wording changed in Git 2.55.
References
- git-blame documentation (The Git project)
- git-log documentation (pickaxe and -L) (The Git project)
- git-bisect documentation (The Git project)
- git-grep documentation (The Git project)
- git-last-modified documentation (The Git project)
- gitattributes: defining a custom hunk-header (The Git project)
- builtin/bisect.c: how git bisect run reads exit statuses (The Git project)
- Git 2.52 release notes (The Git project)
- Git 2.53 release notes (The Git project)
- Git 2.55 release notes (The Git project)
- Git 2.56 release notes (The Git project)
- Viewing and understanding files: ignore commits in the blame view (GitHub)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress