Git & GitHub Module 3 – Exploring history
Read history with git log, show and diff
Filter and format git log, name commits with HEAD~2, HEAD^2 and @{u}, compare A..B with A...B, and inspect any commit or old file with git show.
What you will learn
- Filter git log by author, date, path and message, and format it as one line per commit, a graph or your own fields
- Name commits with revision syntax such as HEAD~2, HEAD^2, @{upstream} and tag names
- Compare ranges with A..B and A...B in git log and git diff
- Inspect a commit with git show and summarise contributors with git shortlog
Before you start
What you’ll learn · Before you start
What you will learn
- Filter git log by author, date, path and message, and format it as one line per commit, a graph or your own fields
- Name commits with revision syntax such as HEAD~2, HEAD^2, @{upstream} and tag names
- Compare ranges with A..B and A...B in git log and git diff
- Inspect a commit with git show and summarise contributors with git shortlog
Before you start
On this page
Git keeps every commit you make, and three commands read them back. git log lists commits, newest first, and
narrows the list by author, date, message or file. git show prints one commit: who made it, when, why and the
change itself; given a path, it prints a file as it was in that commit. git diff compares two snapshots, such
as two branches, or a tag and the latest commit. All three accept the same names for commits: HEAD~2 for two
commits back, v1.0 for a tag, @{u} for the shared branch yours follows. git log also accepts ranges, so
main..reminders lists what reminders has and main does not.
You read history to answer practical questions: what changed since the last release, who knows this file, which commits a merge will bring in, what you have not pushed yet. This lesson runs every command on one small repository and shows what Git printed, so you can predict each answer before you type the command.
One repository for every example
The examples read the history of a class timetable that four students keep in Git: Asha, Ravi, Meera and Imran. The
repository holds timetable.csv, exams.csv and a README on the main branch. Imran added lab sessions on a
branch called labs, and Meera a holiday list on a branch called holidays; both branches were merged into main.
The first finished timetable carries the tag v1.0. After the holidays merge, main was pushed to a shared copy
named origin. Since then Imran has started a branch called reminders, not merged yet, and Ravi and Meera have
made two more commits on main that nobody has pushed. In all, 19 commits.
// Builds the repository every example of this lesson reads: a college class timetable kept in Git by four people,
// with two merged branches, one branch still open, a tag and a remote. Names, emails and dates are fixed (and the
// timezone is India's, +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. Nothing here uses the network; the remote is a folder.
import { spawnSync } from 'node:child_process';
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
export const REPO = 'timetable';
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.
const BASE_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 many commands below much faster. Any other system runs plain `git`.
const GIT = (() => {
const dir = spawnSync('git', ['--exec-path'], { encoding: 'utf8' }).stdout?.trim();
return dir && existsSync(join(dir, 'git')) ? join(dir, 'git') : 'git';
})();
/** The environment of a command run by `who` at `when` (India time). */
function as(who, when) {
const [, name, email] = /^(.*) <(.*)>$/.exec(PEOPLE[who]);
const date = `${when}:00+05:30`;
return { ...BASE_ENV, GIT_AUTHOR_NAME: name, GIT_AUTHOR_EMAIL: email, GIT_AUTHOR_DATE: date, GIT_COMMITTER_NAME: name, GIT_COMMITTER_EMAIL: email, GIT_COMMITTER_DATE: date };
}
function run(args, env = BASE_ENV, cwd = REPO) {
const r = spawnSync(GIT, args, { cwd, env, encoding: 'utf8' });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed:\n${r.stderr}`);
return r.stdout;
}
const files = {};
/** Writes the files of a step and commits them as `who` at `when`. */
function commit(who, when, message, changes) {
for (const [path, text] of Object.entries(changes)) {
files[path] = text;
writeFileSync(join(REPO, path), text);
}
run(['add', '--all']);
run(['commit', '--quiet', '--message', message], as(who, when));
}
const timetable = (rows) => ['day,period,course,room', ...rows].join('\n') + '\n';
const week1 = ['Mon,1,Data Structures,201', 'Mon,2,Discrete Maths,105', 'Tue,1,Operating Systems,203', 'Tue,2,Physics,Lab A'];
const week2 = ['Wed,1,Data Structures,201', 'Wed,2,Hindi,105', 'Thu,1,Operating Systems,203', 'Fri,1,Discrete Maths,201', 'Fri,2,Physics,201'];
/** Creates ./timetable (and ./origin.git, its remote) from nothing. */
export function buildTimetable() {
rmSync(REPO, { recursive: true, force: true });
rmSync('origin.git', { recursive: true, force: true });
mkdirSync(REPO);
run(['init', '--quiet', '--initial-branch=main']);
commit('Asha', '2026-01-05T09:30', 'Start the timetable with a README', {
'README.md': '# Timetable\n\nClasses and exams of the second-year computer science batch.\n',
});
commit('Asha', '2026-01-06T10:15', 'Add Monday and Tuesday classes', { 'timetable.csv': timetable(week1) });
commit('Ravi', '2026-01-08T16:40', 'Add Wednesday to Friday classes', { 'timetable.csv': timetable([...week1, ...week2]) });
commit('Meera', '2026-01-12T11:05', 'Add the mid-semester exam schedule', {
'exams.csv': 'date,course,hall\n2026-03-16,Data Structures,Hall A\n2026-03-18,Operating Systems,Hall A\n2026-03-20,Physics,Hall C\n',
});
const moved = [...week1, ...week2].map((r) => r.replace('Operating Systems,203', 'Operating Systems,204'));
commit('Asha', '2026-01-13T09:00', 'Move Operating Systems to room 204', { 'timetable.csv': timetable(moved) });
run(['tag', '--annotate', 'v1.0', '--message', 'First timetable of the semester'], as('Asha', '2026-01-13T09:05'));
// A branch for the lab sessions, merged a week later.
run(['switch', '--quiet', '--create', 'labs']);
commit('Imran', '2026-01-19T14:20', 'Add lab sessions for both batches', { 'labs.csv': 'day,batch,lab\nTue,A,Lab 1\nThu,B,Lab 1\n' });
commit('Imran', '2026-01-21T15:10', 'Book Lab 2 for the Thursday batch', { 'labs.csv': 'day,batch,lab\nTue,A,Lab 1\nThu,B,Lab 2\n' });
run(['switch', '--quiet', 'main']);
const friday = moved.map((r) => r.replace('Fri,1,Discrete Maths,201', 'Fri,1,Discrete Maths,105'));
commit('Ravi', '2026-01-20T10:30', 'Fix the Friday room numbers', { 'timetable.csv': timetable(friday) });
run(['merge', '--quiet', '--no-ff', '--message', "Merge branch 'labs'", 'labs'], as('Asha', '2026-01-22T12:00'));
commit('Meera', '2026-02-02T09:45', 'Shift the Physics exam to Hall B', {
'exams.csv': files['exams.csv'].replace('Physics,Hall C', 'Physics,Hall B'),
});
commit('Ravi', '2026-02-09T18:20', 'List the exam rules in the README', {
'README.md': `${files['README.md']}\nExam rules: bring your ID card; phones stay outside the hall.\n`,
});
// A second branch, for the holiday list.
run(['switch', '--quiet', '--create', 'holidays']);
commit('Meera', '2026-02-16T11:30', 'Add the holiday list', { 'holidays.csv': 'date,holiday\n2026-01-26,Republic Day\n' });
commit('Meera', '2026-02-18T10:10', 'Add Ambedkar Jayanti to the holiday list', { 'holidays.csv': `${files['holidays.csv']}2026-04-14,Ambedkar Jayanti\n` });
run(['switch', '--quiet', 'main']);
commit('Asha', '2026-02-17T17:00', 'Add Saturday tutorial slots', { 'timetable.csv': timetable([...friday, 'Sat,1,Tutorial,105']) });
run(['merge', '--quiet', '--no-ff', '--message', "Merge branch 'holidays'", 'holidays'], as('Asha', '2026-02-19T09:00'));
// The shared copy: a bare repository in a folder, pushed to once.
run(['init', '--quiet', '--bare', 'origin.git'], BASE_ENV, '.');
run(['remote', 'add', 'origin', '../origin.git']);
run(['push', '--quiet', '--set-upstream', 'origin', 'main']);
// Work after the push: a branch that is not merged yet, and two commits on main that are not pushed yet.
run(['switch', '--quiet', '--create', 'reminders']);
commit('Imran', '2026-03-02T20:15', "Add a script that lists tomorrow's classes", {
'reminders.mjs': "import { readFileSync } from 'node:fs';\n\nconst day = process.argv[2] ?? 'Mon';\nfor (const row of readFileSync('timetable.csv', 'utf8').trim().split('\\n').slice(1)) {\n if (row.startsWith(`${day},`)) console.log(row);\n}\n",
});
commit('Imran', '2026-03-04T21:00', 'Send the reminders at 7 AM India time', {
'reminders.mjs': `${files['reminders.mjs']}\n// Scheduled for 07:00 IST (01:30 UTC) by the college server.\n`,
});
run(['switch', '--quiet', 'main']);
commit('Ravi', '2026-03-03T10:00', 'Move the Data Structures exam a week later', {
'exams.csv': files['exams.csv'].replace('2026-03-16,Data Structures', '2026-03-23,Data Structures'),
});
commit('Meera', '2026-03-05T12:30', 'Add the end-semester exam schedule', {
'exams.csv': `${files['exams.csv']}2026-05-04,Data Structures,Hall A\n2026-05-06,Operating Systems,Hall B\n`,
});
}
/**
* Prints a command the way a terminal shows it, then runs it in the repository and prints what Git printed.
* Quotes work as in a shell: show(`git log --format='%h %s'`).
*/
export function show(command) {
const args = words(command);
if (args.shift() !== 'git') throw new Error(`not a git command: ${command}`);
const { status, text } = capture(args);
console.log(`$ ${command}`);
if (text) console.log(text);
if (status !== 0) console.log(`(exit status ${status})`);
console.log('');
}
/** Runs git with its output and its error messages going to one file, so they stay in the order Git wrote them. */
function capture(args) {
const file = 'git-output.tmp';
const fd = openSync(file, 'w');
try {
const r = spawnSync(GIT, args, { cwd: REPO, env: BASE_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;
} Each example is a short Node.js program that builds this repository with real Git, then runs Git commands in it and
prints each one after a $ sign, as a terminal shows it, followed by what Git printed. Names, email addresses and
dates are fixed, in India Standard Time (UTC+05:30), so the commit IDs come out the same on every computer. To
follow along, put timetable.mjs and any example in an empty folder, run it with node, and then work in the
timetable folder it leaves behind.
The default log, and one line per commit
// The first look at a history: git log in its default form, then one line per commit.
// Run it with Node.js 24 in a folder that holds timetable.mjs: node log_basics.mjs
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show('git --version');
show('git log -2');
show('git log --oneline'); Output
$ git --version
git version 2.54.0 (Apple Git-157)
$ git log -2
commit 482c24cfd77e26040cd9251b07de2e4bf7637d0e
Author: Meera Iyer <meera@example.com>
Date: Thu Mar 5 12:30:00 2026 +0530
Add the end-semester exam schedule
commit 7deb62fd768f7a03a0587af9b22a3922b228588e
Author: Ravi Kumar <ravi@example.com>
Date: Tue Mar 3 10:00:00 2026 +0530
Move the Data Structures exam a week later
$ git log --oneline
482c24c Add the end-semester exam schedule
7deb62f Move the Data Structures exam a week later
f7c61f5 Merge branch 'holidays'
95ac0e7 Add Ambedkar Jayanti to the holiday list
5bc127d Add Saturday tutorial slots
74349c8 Add the holiday list
8936436 List the exam rules in the README
9b26674 Shift the Physics exam to Hall B
1ecdac6 Merge branch 'labs'
7b5b101 Book Lab 2 for the Thursday batch
a393545 Fix the Friday room numbers
8e5c05a Add lab sessions for both batches
b2bb47d Move Operating Systems to room 204
98fb14a Add the mid-semester exam schedule
a7d97a2 Add Wednesday to Friday classes
8b420d8 Add Monday and Tuesday classes
cebbba9 Start the timetable with a README
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node log_basics.mjs
git log -2 shows the two newest commits in the default format. Each starts with the commit’s full ID, 40
hexadecimal characters, then the author, the date with the author’s time zone, and the message, indented. Commits
come newest first, by commit date. -2 is short for --max-count=2. When the output is longer than the screen, a
terminal shows it in a pager, usually less, where Space moves down and q quits; git --no-pager log prints it
straight through.
git log --oneline gives each commit one line: the first seven characters of its ID and the first line of its
message. Any unique beginning of an ID names the commit, so git show 7deb62f works as well as the full ID.
Count the lines: 17, while the repository holds 19 commits. git log starts at HEAD, the commit you have checked
out, and walks back through parents, so it shows only commits that HEAD can reach. The two commits of reminders
are not among them.
Drawing branches with --graph
// Every branch at once, drawn as a graph, and then only the main line of history.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show('git log --oneline --graph --all --decorate');
show('git log --oneline --first-parent'); Output
$ git log --oneline --graph --all --decorate * 482c24c (HEAD -> main) Add the end-semester exam schedule * 7deb62f Move the Data Structures exam a week later | * 71727f4 (reminders) Send the reminders at 7 AM India time | * 323c3ec Add a script that lists tomorrow's classes |/ * f7c61f5 (origin/main) Merge branch 'holidays' |\ | * 95ac0e7 (holidays) Add Ambedkar Jayanti to the holiday list | * 74349c8 Add the holiday list * | 5bc127d Add Saturday tutorial slots |/ * 8936436 List the exam rules in the README * 9b26674 Shift the Physics exam to Hall B * 1ecdac6 Merge branch 'labs' |\ | * 7b5b101 (labs) Book Lab 2 for the Thursday batch | * 8e5c05a Add lab sessions for both batches * | a393545 Fix the Friday room numbers |/ * b2bb47d (tag: v1.0) Move Operating Systems to room 204 * 98fb14a Add the mid-semester exam schedule * a7d97a2 Add Wednesday to Friday classes * 8b420d8 Add Monday and Tuesday classes * cebbba9 Start the timetable with a README $ git log --oneline --first-parent 482c24c Add the end-semester exam schedule 7deb62f Move the Data Structures exam a week later f7c61f5 Merge branch 'holidays' 5bc127d Add Saturday tutorial slots 8936436 List the exam rules in the README 9b26674 Shift the Physics exam to Hall B 1ecdac6 Merge branch 'labs' a393545 Fix the Friday room numbers b2bb47d Move Operating Systems to room 204 98fb14a Add the mid-semester exam schedule a7d97a2 Add Wednesday to Friday classes 8b420d8 Add Monday and Tuesday classes cebbba9 Start the timetable with a README
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node graph.mjs
--graph draws the shape of the history at the left: each * is a commit, | continues a line of history, |\
opens a merge’s second parent and |/ joins a branch back to the commit it started from. --all adds every branch,
tag and remote-tracking branch as a starting point, which brings in the reminders commits. --decorate writes the
names that point at commits in brackets: HEAD -> main (you are on main), origin/main (where main was when it
was last pushed), tag: v1.0 and the branch names. A terminal shows these names without the option, but output
sent to a file or to another program leaves them out, which is why the example asks for them.
--first-parent follows only the first parent of each merge, the branch that was checked out when the merge was
made. The labs and holidays commits drop out, and 13 commits remain: the story of main itself, with each merge as a
single step. Release notes and audits of the main line are usually built this way.
Filtering by author, date, message and file
// Narrowing the history down: by author, by date, by words in the message and by file, alone and together.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show('git log --oneline --author=Meera');
show('git log --oneline --author=Meera --author=Imran');
show('git log --oneline --since=2026-03-01');
show('git log --oneline --since=2026-03-01 --all');
show('git log --oneline -i --grep=exam');
show('git log --oneline -- exams.csv');
show('git log --oneline --author=Meera --since=2026-02-01 -- exams.csv'); Output
$ git log --oneline --author=Meera 482c24c Add the end-semester exam schedule 95ac0e7 Add Ambedkar Jayanti to the holiday list 74349c8 Add the holiday list 9b26674 Shift the Physics exam to Hall B 98fb14a Add the mid-semester exam schedule $ git log --oneline --author=Meera --author=Imran 482c24c Add the end-semester exam schedule 95ac0e7 Add Ambedkar Jayanti to the holiday list 74349c8 Add the holiday list 9b26674 Shift the Physics exam to Hall B 7b5b101 Book Lab 2 for the Thursday batch 8e5c05a Add lab sessions for both batches 98fb14a Add the mid-semester exam schedule $ git log --oneline --since=2026-03-01 482c24c Add the end-semester exam schedule 7deb62f Move the Data Structures exam a week later $ git log --oneline --since=2026-03-01 --all 482c24c Add the end-semester exam schedule 71727f4 Send the reminders at 7 AM India time 7deb62f Move the Data Structures exam a week later 323c3ec Add a script that lists tomorrow's classes $ git log --oneline -i --grep=exam 482c24c Add the end-semester exam schedule 7deb62f Move the Data Structures exam a week later 8936436 List the exam rules in the README 9b26674 Shift the Physics exam to Hall B 98fb14a Add the mid-semester exam schedule $ git log --oneline -- exams.csv 482c24c Add the end-semester exam schedule 7deb62f Move the Data Structures exam a week later 9b26674 Shift the Physics exam to Hall B 98fb14a Add the mid-semester exam schedule $ git log --oneline --author=Meera --since=2026-02-01 -- exams.csv 482c24c Add the end-semester exam schedule 9b26674 Shift the Physics exam to Hall B
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node filters.mjs
--author=Meerakeeps commits whose author line matches a regular expression; the line holds the name and the email address, so a part of either is enough. Given twice,--authorkeeps commits that match either pattern.--sinceand--until, also spelled--afterand--before, filter by commit date. They accept dates such as2026-03-01and phrases such as"2 weeks ago". With--since=2026-03-01the log shows two commits;--alladds the two commits ofreminders, which are just as recent.--grep=examsearches commit messages;-iignores case. Like--author, several--grepoptions keep commits that match any of them, unless--all-matchasks for every one.-- exams.csvkeeps commits that changed that file. The--tells Git that what follows are paths, not branch names. It matters when a file and a branch share a name, or when the file no longer exists.
Different kinds of filter combine: a commit must pass all of them. The last command asks for Meera’s commits since
2026-02-01 that changed exams.csv, and gets two.
Choosing what each commit shows
// Choosing what each commit shows: your own format, the files it changed, or the whole change.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show("git log -4 --format='%h %ad %an: %s' --date=short");
show('git log -1 --stat');
show("git log -3 --name-status --format='%h %s'");
show('git log -1 --patch'); Output
$ git log -4 --format='%h %ad %an: %s' --date=short
482c24c 2026-03-05 Meera Iyer: Add the end-semester exam schedule
7deb62f 2026-03-03 Ravi Kumar: Move the Data Structures exam a week later
f7c61f5 2026-02-19 Asha Rao: Merge branch 'holidays'
95ac0e7 2026-02-18 Meera Iyer: Add Ambedkar Jayanti to the holiday list
$ git log -1 --stat
commit 482c24cfd77e26040cd9251b07de2e4bf7637d0e
Author: Meera Iyer <meera@example.com>
Date: Thu Mar 5 12:30:00 2026 +0530
Add the end-semester exam schedule
exams.csv | 2 ++
1 file changed, 2 insertions(+)
$ git log -3 --name-status --format='%h %s'
482c24c Add the end-semester exam schedule
M exams.csv
7deb62f Move the Data Structures exam a week later
M exams.csv
f7c61f5 Merge branch 'holidays'
$ git log -1 --patch
commit 482c24cfd77e26040cd9251b07de2e4bf7637d0e
Author: Meera Iyer <meera@example.com>
Date: Thu Mar 5 12:30:00 2026 +0530
Add the end-semester exam schedule
diff --git a/exams.csv b/exams.csv
index c32dc7b..2d9adec 100644
--- a/exams.csv
+++ b/exams.csv
@@ -2,3 +2,5 @@ date,course,hall
2026-03-23,Data Structures,Hall A
2026-03-18,Operating Systems,Hall A
2026-03-20,Physics,Hall B
+2026-05-04,Data Structures,Hall A
+2026-05-06,Operating Systems,Hall B
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node formats.mjs
--format builds each line from placeholders: %h is the short ID and %H the full one, %an the author’s
name and %ae their email address, %ad the author date, %s the subject (the first line of the message) and %b
the rest of it. --date=short prints dates as year, month and day. Scripts should read history this way: the
default output changes with settings, terminal width and Git version, while a format string prints exactly what it
asks for. For a time you want to convert, %at gives the author date as a Unix timestamp.
The other options add what each commit changed:
--stat: the files, with a count of changed lines.--name-status: the files, each with a letter:Mmodified,Aadded,Ddeleted,Rrenamed.-por--patch: the whole change, line by line, in the diff format thatgit diffalso prints.
The merge f7c61f5 lists no files. git log shows no change for merge commits unless you ask for one, for example
with --first-parent, which shows each merge as a change against its first parent.
Names for commits
Every command in this lesson takes a revision, a name for a commit. Branch and tag names are revisions, and so is any commit ID. Suffixes walk from one commit to another, and the diagram puts the names of the next example on the newest commits of the repository.
Names for commits, and what each side has alone
Text description of the diagram
The diagram draws the newest commits of the timetable repository as boxes, newest at the top. Each arrow points from a commit to its parent.
- Two groups sit side by side at the top. The group labelled "reminders..main" holds the commits only main has: 482c24c, where main and HEAD point, and its parent 7deb62f, HEAD~1. The group labelled "main..reminders" holds the commits only the reminders branch has: 71727f4, its tip, and its parent 323c3ec, reminders~1.
- Both groups point down to f7c61f5, a merge commit drawn with a double border. It is HEAD~2, it is where the upstream @{u} (origin/main) points, and it is the merge base of main and reminders: the newest commit both branches share.
- The merge has two parents. Its first parent is 5bc127d, which HEAD~2^1 and HEAD~3 both name. Its second parent is 95ac0e7, which HEAD~2^2 names and the holidays branch points at; below it comes 74349c8.
- Both lines join at a dashed box for 8936436 and the 11 older commits, the v1.0 tag among them.
main...reminders, with three dots, is both groups at the top together: the four commits that only one of the two branches has.
// Names for commits: HEAD and its ancestors, the parents of a merge, a tag, the upstream branch, and a file as it
// was at a commit.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
for (const name of ['HEAD', 'HEAD~1', 'HEAD~2', 'HEAD~2^1', 'HEAD~2^2', 'HEAD~3', 'v1.0', '@{u}']) {
show(`git log -1 --format='%h %s' ${name}`);
}
show('git log -1 --format=%h HEAD^2');
show('git show v1.0:timetable.csv'); Output
$ git log -1 --format='%h %s' HEAD
482c24c Add the end-semester exam schedule
$ git log -1 --format='%h %s' HEAD~1
7deb62f Move the Data Structures exam a week later
$ git log -1 --format='%h %s' HEAD~2
f7c61f5 Merge branch 'holidays'
$ git log -1 --format='%h %s' HEAD~2^1
5bc127d Add Saturday tutorial slots
$ git log -1 --format='%h %s' HEAD~2^2
95ac0e7 Add Ambedkar Jayanti to the holiday list
$ git log -1 --format='%h %s' HEAD~3
5bc127d Add Saturday tutorial slots
$ git log -1 --format='%h %s' v1.0
b2bb47d Move Operating Systems to room 204
$ git log -1 --format='%h %s' @{u}
f7c61f5 Merge branch 'holidays'
$ git log -1 --format=%h HEAD^2
fatal: ambiguous argument 'HEAD^2': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
(exit status 128)
$ git show v1.0:timetable.csv
day,period,course,room
Mon,1,Data Structures,201
Mon,2,Discrete Maths,105
Tue,1,Operating Systems,204
Tue,2,Physics,Lab A
Wed,1,Data Structures,201
Wed,2,Hindi,105
Thu,1,Operating Systems,204
Fri,1,Discrete Maths,201
Fri,2,Physics,201
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node revisions.mjs
~nwalks back n generations, always taking the first parent.HEAD~1is the parent ofHEAD,HEAD~2its grandparent, which here is the holidays merge.HEAD~alone meansHEAD~1.^npicks the nth parent of one commit. It matters for merges, which have two:HEAD~2^1is the commitmainwas on, the same commit asHEAD~3, andHEAD~2^2is the tip of the mergedholidaysbranch.HEAD^alone meansHEAD^1, so for an ordinary commitHEAD^andHEAD~are the same commit. Asking forHEAD^2on a commit with one parent fails: Git cannot resolve the name and finds no file of that name either, so it reports an ambiguous argument.- A tag names the commit it was made on.
v1.0resolves tob2bb47d, the commit Asha tagged. @{u}, short for@{upstream}, is the remote-tracking branch your branch follows, the onegit pullmerges from and, in the usual setup,git pushsends to. Here that isorigin/main, which still points at the holidays merge, because the two newest commits have not been pushed.main@{u}asks the same for a named branch.<rev>:<path>names a file as it was at a revision.git show v1.0:timetable.csvprints the timetable as it was tagged, with Discrete Maths still in room 201 on Friday, before Ravi’s fix.
Two more forms look similar and mean something else. main@{1} reads your reflog, the local record of where a
branch pointed before each move, and names where main was before its latest change; another computer’s reflog
differs. HEAD@{2} does the same for HEAD. git rev-parse prints the full ID behind any name, which is the quickest
way to check that a name means what you think.
Ranges: two dots and three dots
// Two dots and three dots in git log: what one branch has that another lacks, and what either has alone.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show('git log --oneline main..reminders');
show('git log --oneline reminders..main');
show('git log --oneline --left-right main...reminders');
show('git merge-base main reminders');
show('git log --oneline @{u}..');
show('git log --oneline ..@{u}'); Output
$ git log --oneline main..reminders
71727f4 Send the reminders at 7 AM India time
323c3ec Add a script that lists tomorrow's classes
$ git log --oneline reminders..main
482c24c Add the end-semester exam schedule
7deb62f Move the Data Structures exam a week later
$ git log --oneline --left-right main...reminders
< 482c24c Add the end-semester exam schedule
> 71727f4 Send the reminders at 7 AM India time
< 7deb62f Move the Data Structures exam a week later
> 323c3ec Add a script that lists tomorrow's classes
$ git merge-base main reminders
f7c61f5175cb05827285695b09fd497809177938
$ git log --oneline @{u}..
482c24c Add the end-semester exam schedule
7deb62f Move the Data Structures exam a week later
$ git log --oneline ..@{u}
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ranges.mjs
A..B lists the commits that B can reach and A cannot. main..reminders gives the two commits of reminders;
turn it round, reminders..main, and you get the two commits main has gained since reminders started. Order
matters. A..B is a shorter way to write ^A B, meaning B, excluding what A reaches.
A...B lists the commits that exactly one of the two can reach, the four commits on either side of the fork.
With --left-right, Git marks each one < when it comes from the left name and > when it comes from the right.
git merge-base main reminders prints the newest commit both share, f7c61f5, the point where reminders started.
Leave out one end and Git puts HEAD there. @{u}.. therefore means @{u}..HEAD: your commits that the upstream
does not have yet, the two you would push. ..@{u} lists the opposite, commits on the upstream that you have not
merged; it is empty here. The upstream is only as fresh as your last git fetch, so fetch before you trust it.
Two and three dots in git diff
git diff takes the same two spellings, but compares two snapshots instead of listing commits, and that changes
what the dots mean.
// The same two branches compared with git diff: two dots compare the tips, three dots start at the merge base.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show('git diff --stat main..reminders');
show('git diff --stat main...reminders');
show('git diff main..reminders -- exams.csv'); Output
$ git diff --stat main..reminders exams.csv | 4 +--- reminders.mjs | 8 ++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) $ git diff --stat main...reminders reminders.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) $ git diff main..reminders -- exams.csv diff --git a/exams.csv b/exams.csv index 2d9adec..f95f067 100644 --- a/exams.csv +++ b/exams.csv @@ -1,6 +1,4 @@ date,course,hall -2026-03-23,Data Structures,Hall A +2026-03-16,Data Structures,Hall A 2026-03-18,Operating Systems,Hall A 2026-03-20,Physics,Hall B -2026-05-04,Data Structures,Hall A -2026-05-06,Operating Systems,Hall B
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node diff_ranges.mjs
git diff main..reminders is the same as git diff main reminders: it compares the snapshot at the tip of
main with the snapshot at the tip of reminders. The result contains the new reminders.mjs, and also two exam
changes, shown in reverse. reminders never touched exams.csv; main changed it after reminders started, so
going from one tip to the other undoes those changes. Read as a review of the branch, this diff is wrong.
git diff main...reminders starts at the merge base: it compares f7c61f5 with the tip of reminders and shows
only what the branch itself changed, the 8 lines of reminders.mjs. This is the comparison a pull request on GitHub
shows. The Git manual says it plainly: git diff compares two endpoints, not ranges. To review a branch, use three
dots; to compare two snapshots, use two names.
One commit at a time: git show
// git show for an ordinary commit, for a tag, and for a merge (which shows no change of its own).
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show('git show HEAD~1');
show('git show --no-patch v1.0');
show('git show HEAD~2');
show('git show --first-parent --stat HEAD~2'); Output
$ git show HEAD~1
commit 7deb62fd768f7a03a0587af9b22a3922b228588e
Author: Ravi Kumar <ravi@example.com>
Date: Tue Mar 3 10:00:00 2026 +0530
Move the Data Structures exam a week later
diff --git a/exams.csv b/exams.csv
index f95f067..c32dc7b 100644
--- a/exams.csv
+++ b/exams.csv
@@ -1,4 +1,4 @@
date,course,hall
-2026-03-16,Data Structures,Hall A
+2026-03-23,Data Structures,Hall A
2026-03-18,Operating Systems,Hall A
2026-03-20,Physics,Hall B
$ git show --no-patch v1.0
tag v1.0
Tagger: Asha Rao <asha@example.com>
Date: Tue Jan 13 09:05:00 2026 +0530
First timetable of the semester
commit b2bb47de17472f40d9100f88ca7fa93f41b6bc06
Author: Asha Rao <asha@example.com>
Date: Tue Jan 13 09:00:00 2026 +0530
Move Operating Systems to room 204
$ git show HEAD~2
commit f7c61f5175cb05827285695b09fd497809177938
Merge: 5bc127d 95ac0e7
Author: Asha Rao <asha@example.com>
Date: Thu Feb 19 09:00:00 2026 +0530
Merge branch 'holidays'
$ git show --first-parent --stat HEAD~2
commit f7c61f5175cb05827285695b09fd497809177938
Merge: 5bc127d 95ac0e7
Author: Asha Rao <asha@example.com>
Date: Thu Feb 19 09:00:00 2026 +0530
Merge branch 'holidays'
holidays.csv | 3 +++
1 file changed, 3 insertions(+)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node show.mjs
For an ordinary commit, git show prints the same header as git log followed by the change as a patch. For an
annotated tag, it prints the tag itself (who tagged, when and with what message) and then the commit the tag points
at; --no-patch leaves out the change.
For a merge, git show prints no change at all. It uses a combined diff, which leaves out every part of the result
that matches one of the parents, so it shows only what the merge itself changed, usually a conflict someone resolved
by hand. A clean merge such as f7c61f5 has nothing to show. To see what a merge brought into main, compare it
with its first parent: git show --first-parent does that, and with --stat it shows that the merge added three
lines to holidays.csv.
Who did what: git shortlog
// Who made how many commits: git shortlog, without a revision, with HEAD, without merges and for every branch.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show('git shortlog -sn');
show('git shortlog -sn HEAD');
show('git shortlog -sn --no-merges HEAD');
show('git shortlog -sn --all'); Output
$ git shortlog -sn
$ git shortlog -sn HEAD
6 Asha Rao
5 Meera Iyer
4 Ravi Kumar
2 Imran Sheikh
$ git shortlog -sn --no-merges HEAD
5 Meera Iyer
4 Asha Rao
4 Ravi Kumar
2 Imran Sheikh
$ git shortlog -sn --all
6 Asha Rao
5 Meera Iyer
4 Imran Sheikh
4 Ravi Kumar
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node shortlog.mjs
git shortlog groups commits by author. -s prints only the count for each person, -n sorts by that count, and
-e adds email addresses. Asha has 6 commits on main, but 2 of them are merges; --no-merges leaves those out.
--all counts every branch, which adds Imran’s two commits on reminders.
The first command printed nothing, and that is the trap. When it gets no revision and its input does not come from a
terminal, git shortlog reads git log output from its standard input instead of the repository. A script or a CI
job therefore gets an empty summary, so pass HEAD or a range explicitly. When one person appears under two
spellings, say a work and a personal email address, a .mailmap file at the top of the repository tells
git shortlog to count them as one.
Version note
The examples ran on Git 2.54.0. Git 2.55 added --graph-lane-limit=<n> to git log, which caps the number of lanes
the graph draws and marks the cut with ~, and --max-count-oldest=<n>, which keeps the oldest n commits instead
of the newest. Git 2.56 draws root commits, those without a parent, indented in --graph output, so that a history
started afresh, such as an unrelated branch, no longer looks attached to the commit above it; --no-graph-indent or
the log.graphIndent setting turns that off. This repository’s one root commit is the last line of its graph, and
Git 2.56 indents a root only when more commits follow it, so the graph above prints the same there.
Try it
Run any example once, change into the timetable folder it leaves, and answer these with one command each:
- Which commits changed
labs.csv, and who made them? - What did
timetable.csvcontain two commits beforev1.0? - Which commits came from the
labsbranch, counting fromv1.0? - What would
git pushsend right now? - What would merging
reminderschange, file by file?
One set of answers, run on the same repository: git log --format='%h %an %s' -- labs.csv,
git show v1.0~2:timetable.csv, git log --oneline v1.0..labs, git log --oneline @{u}.. and
git diff --stat main...reminders.
// One set of answers to the questions of "Try it", run on the same repository.
import { buildTimetable, show } from './timetable.mjs';
buildTimetable();
show("git log --format='%h %an %s' -- labs.csv");
show('git show v1.0~2:timetable.csv');
show('git log --oneline v1.0..labs');
show('git log --oneline @{u}..');
show('git diff --stat main...reminders'); Output
$ git log --format='%h %an %s' -- labs.csv
7b5b101 Imran Sheikh Book Lab 2 for the Thursday batch
8e5c05a Imran Sheikh Add lab sessions for both batches
$ git show v1.0~2:timetable.csv
day,period,course,room
Mon,1,Data Structures,201
Mon,2,Discrete Maths,105
Tue,1,Operating Systems,203
Tue,2,Physics,Lab A
Wed,1,Data Structures,201
Wed,2,Hindi,105
Thu,1,Operating Systems,203
Fri,1,Discrete Maths,201
Fri,2,Physics,201
$ git log --oneline v1.0..labs
7b5b101 Book Lab 2 for the Thursday batch
8e5c05a Add lab sessions for both batches
$ git log --oneline @{u}..
482c24c Add the end-semester exam schedule
7deb62f Move the Data Structures exam a week later
$ git diff --stat main...reminders
reminders.mjs | 8 ++++++++
1 file changed, 8 insertions(+)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node try_it.mjs
In the second answer, Operating Systems is still in room 203: Asha moved it to 204 in the commit she then tagged.
Other spellings work too: HEAD~10:timetable.csv and a7d97a2:timetable.csv name the same file.
Practise: names and ranges in code
Git works out every name and range in this lesson by walking parent links. Write that walk for the commit graph of the timetable repository; the sample tests compare your answers with what Git printed.
Exercise · Medium · JavaScript
Work out ~n, ^n, A..B and A...B on a commit graph
Git answers HEAD~2, HEAD^2, main..reminders and main...reminders by walking from commit to parent. Write that walk yourself, for the timetable repository of this lesson.
A commit graph is given as an object: each commit's short ID maps to the list of its parents, first parent first, so the merge f7c61f5 maps to ['5bc127d', '95ac0e7'] and the first commit maps to []. reachable(graph, id) is already written: it returns a Set of every commit you can reach from id, id included.
Write four functions in revisions.mjs and export them:
ancestor(graph, id, n)returns the commit thatid~nnames: follow first parentsntimes. It returnsnullwhen the walk runs past the first commit.parent(graph, id, n)returns the commit thatid^nnames: thenth parent, oriditself whennis 0. It returnsnullwhen the commit has no such parent.twoDot(graph, a, b)returns the commitsgit log a..blists, as an array in any order.threeDot(graph, a, b)returns the commitsgit log a...blists, as an array in any order.
The sample tests use the 19 commits of the repository, with the IDs the examples printed, and the answers real Git gave for the same names and ranges.
Starter code · revisions.mjs
/** Every commit reachable from `id` by parent links, `id` included (already written). */
export function reachable(graph, id) {
const seen = new Set();
const todo = [id];
while (todo.length > 0) {
const commit = todo.pop();
if (seen.has(commit)) continue;
seen.add(commit);
todo.push(...graph[commit]);
}
return seen;
}
/** id~n: follow first parents n times (null past the first commit). */
export function ancestor(graph, id, n) {
// Replace this line with your code.
return null;
}
/** id^n: the nth parent, or id itself for n = 0 (null when there is no such parent). */
export function parent(graph, id, n) {
// Replace this line with your code.
return null;
}
/** What git log a..b lists: reachable from b, not from a. */
export function twoDot(graph, a, b) {
// Replace this line with your code.
return [];
}
/** What git log a...b lists: reachable from a or from b, but not from both. */
export function threeDot(graph, a, b) {
// Replace this line with your code.
return [];
} The sample tests · revisions.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { ancestor, parent, threeDot, twoDot } from './revisions.mjs';
// The timetable repository of the lesson: each commit's short ID and its parents, first parent first.
const graph = {
'482c24c': ['7deb62f'],
'7deb62f': ['f7c61f5'],
'71727f4': ['323c3ec'],
'323c3ec': ['f7c61f5'],
'f7c61f5': ['5bc127d', '95ac0e7'],
'95ac0e7': ['74349c8'],
'74349c8': ['8936436'],
'5bc127d': ['8936436'],
'8936436': ['9b26674'],
'9b26674': ['1ecdac6'],
'1ecdac6': ['a393545', '7b5b101'],
'7b5b101': ['8e5c05a'],
'8e5c05a': ['b2bb47d'],
'a393545': ['b2bb47d'],
'b2bb47d': ['98fb14a'],
'98fb14a': ['a7d97a2'],
'a7d97a2': ['8b420d8'],
'8b420d8': ['cebbba9'],
'cebbba9': [],
};
// Where the branches, the tag and the upstream point (git rev-parse --short).
const main = '482c24c';
const reminders = '71727f4';
const labs = '7b5b101';
const holidays = '95ac0e7';
const v1 = 'b2bb47d';
const upstream = 'f7c61f5';
const sorted = (ids) => [...ids].sort();
test('~n follows first parents (main~2, main~3, main~12, reminders~2)', () => {
assert.equal(ancestor(graph, main, 0), main);
assert.equal(ancestor(graph, main, 2), 'f7c61f5');
assert.equal(ancestor(graph, main, 3), '5bc127d');
assert.equal(ancestor(graph, main, 12), 'cebbba9');
assert.equal(ancestor(graph, reminders, 2), 'f7c61f5');
});
test('~n past the first commit is null (main~13)', () => {
assert.equal(ancestor(graph, main, 13), null);
});
test('^n picks one parent of a merge (main~2^1, main~2^2, main~6^2, main^0)', () => {
assert.equal(parent(graph, ancestor(graph, main, 2), 1), '5bc127d');
assert.equal(parent(graph, ancestor(graph, main, 2), 2), '95ac0e7');
assert.equal(parent(graph, ancestor(graph, main, 6), 2), '7b5b101');
assert.equal(parent(graph, main, 0), main);
});
test('^2 of a commit with one parent, and ^1 of the first commit, are null', () => {
assert.equal(parent(graph, main, 2), null);
assert.equal(parent(graph, 'cebbba9', 1), null);
});
test('a..b lists what b reaches and a does not', () => {
assert.deepEqual(sorted(twoDot(graph, main, reminders)), ['323c3ec', '71727f4']);
assert.deepEqual(sorted(twoDot(graph, reminders, main)), ['482c24c', '7deb62f']);
assert.deepEqual(sorted(twoDot(graph, upstream, main)), ['482c24c', '7deb62f']);
assert.deepEqual(sorted(twoDot(graph, v1, upstream)), ['1ecdac6', '5bc127d', '74349c8', '7b5b101', '8936436', '8e5c05a', '95ac0e7', '9b26674', 'a393545', 'f7c61f5']);
assert.deepEqual(sorted(twoDot(graph, main, main)), []);
});
test('a...b lists what exactly one side reaches', () => {
assert.deepEqual(sorted(threeDot(graph, main, reminders)), ['323c3ec', '482c24c', '71727f4', '7deb62f']);
// labs is merged into holidays' history already, so three dots give the same as labs..holidays.
assert.deepEqual(sorted(threeDot(graph, holidays, labs)), ['1ecdac6', '74349c8', '8936436', '95ac0e7', '9b26674', 'a393545']);
assert.deepEqual(sorted(threeDot(graph, labs, holidays)), sorted(twoDot(graph, labs, holidays)));
}); A hint
~n takes the first parent, [0], again and again; ^n reads one entry of the list, [n - 1]. For the ranges, compute reachable() from both ends: a..b keeps what b reaches and a does not, and a...b keeps what exactly one of the two reaches.
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 HEAD~2 and HEAD^2?
HEAD~2 walks two generations back along first parents: the parent of the parent of HEAD. HEAD^2 stays one
generation back and picks the second parent of HEAD, which exists only when HEAD is a merge; otherwise Git
reports an unknown revision. The two can be combined: HEAD~2^2 is the second parent of the commit two steps back,
the tip of a merged branch. For an ordinary commit with one parent, HEAD^ and HEAD~ both mean the first parent,
so the difference shows up only around merges. In practice ~ moves along the main line of history and ^n
chooses which side of a merge to follow.
What does git log main..feature list, and how is main...feature different?
main..feature lists commits reachable from feature but not from main: the work on the branch that main
does not have yet. main...feature lists commits reachable from either but not both, so it adds the commits main
gained after the branch started; --left-right marks which side each one is on. When main has not moved since the
branch started, the two give the same list. In git diff the dots mean something else: two dots compare the two
tips, while three dots compare the merge base with feature, which shows only the branch’s own changes, as a pull
request does.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A teammate reviews a feature branch with git diff main..feature and asks why the branch reverts yesterday's hotfix on main. Nobody touched that code on the branch. Explain what happened, and give the command they should have run.
-
Mid to senior
A release-notes script runs git log between the last two tags and prints commit subjects. It works on your laptop, but in CI it fails with an unknown revision or prints a single commit. What do you check, and how do you make the script reliable?
Key takeaways
git loglists commits newest first fromHEAD;--alladds every branch,--graph --decoratedraws the shape with names, and--first-parentkeeps the main line.- Filters combine:
--authorand--greptake regular expressions,--sinceand--untiltake dates, and-- <path>keeps commits that changed a file. Every kind of filter must match. - For scripts, ask for
--formatplaceholders instead of reading the default output, and givegit shortlogan explicit revision such asHEAD. ~nwalks first parents,^npicks a parent of a merge, a tag names its commit,@{u}is the upstream and<rev>:<path>is a file at a revision.- In
git log,A..Bis what B has and A lacks, andA...Bis what either has alone. Ingit diff, two dots compare the tips and three dots start at the merge base, which is what reviewing a branch needs. git showprints one commit with its change, a tag with its commit, or a file at a revision; for a clean merge it shows no change unless you add--first-parent.
References
- git-log documentation (The Git project)
- git: the command-line interface (--no-pager) (The Git project)
- gitrevisions: specifying revisions and ranges (The Git project)
- git-show documentation (The Git project)
- git-diff documentation (The Git project)
- git-shortlog documentation (The Git project)
- gitmailmap: map author and committer names and email addresses (The Git project)
- Git 2.55 release notes (The Git project)
- Git 2.56 release notes (The Git project)
- About comparing branches in pull requests (GitHub)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress