Menu for Per-Arne's Git Good!
Courses and project Per-Arne's Git Good!

Reading history: log, show and the objects behind them

Module 1: Foundations · 11 min

The interactive lesson needs JavaScript. The text below is the complete lesson.

After this lesson you can

  • use git log with --oneline, --stat and -- <file> to find the commit that changed a specific file apply
  • refer to older commits relative to HEAD with HEAD~1 and HEAD~2, and fetch an old file version with git show apply
  • explain why a commit ID changes when content, parent or message changes understand
  • trace a commit to its tree and blobs with git cat-file -p and git ls-tree, and explain why unchanged files share a blob apply

Understand: What history is made of

git log shows the history with the newest commit at the top: ID, author, time and message. Options control the detail. --oneline gives one line per commit, --stat shows which files changed, and -p shows the changes themselves.

The ID is a hash: a forty-character fingerprint computed from everything in the commit. That includes the snapshot, the parent, the author, the time and the message. Change any of it and you get a different ID. Usually the first seven characters are enough.

You rarely need to type IDs. HEAD~1 means one step back from where you are: the parent. HEAD~2 is two steps back.

What is inside a commit? Commit D points to a tree, the snapshot's table of contents. The tree lists two file names, and each name points to a blob: the content of one file. D also stores its parent's ID, C: the arrow in the graph.

Each commit stores its parent's ID, so the IDs form a chain. Change anything in B and you get a new commit with a new ID. C still points to the old B, so to build on the new one, C and D must be made again, with new IDs. Nobody can change history quietly.

Watch: From log to blob

This project has four commits. --graph draws the line between them. Here it is straight. Once we have branches later on, it will fork.

$ git log --oneline --graph

With --stat we see which files each commit changed. With -- and a file name, git log shows only the commits that changed that file.

$ git log --oneline --stat

git show HEAD~1 shows the parent of the newest commit. We get the ID, author, time and message, and below that the diff against its own parent. Git computes that diff now. It is not stored.

$ git show HEAD~1

Now we see the commit object itself. It points to one tree and one parent. Look at the parent line: it is the ID of the commit we just showed. That line is the arrow in the graph.

$ git cat-file -p HEAD

The tree is the table of contents. Each line has a file name and the ID of a blob. Take note of how the ID for README.md starts.

$ git ls-tree HEAD

The newest commit changed only INSTALL.md. What do you think happens with README.md in the snapshot before it?

$ git ls-tree HEAD~1

HEAD:README.md means the blob for README.md in HEAD. The blob is just the content. It has neither a name nor a date. The name lives in the tree. In the objects view you can see that both trees point to this same blob.

$ git cat-file -p HEAD:README.md

Tasks

  1. Show the history compactly, with the graph.
  2. Show which files each commit changed.
  3. Show the commit right before the newest one, with its changes.
  4. Print the raw commit object of HEAD.
  5. List the files in the newest snapshot, with their blob IDs.
  6. List the files in the snapshot before the newest one, and compare the IDs.
  7. Print the content of README.md as it is stored in the newest commit.

Practise: Transfer: dig into a shopping list

A new repository with five commits. First you read the history and pull out answers. Finally you make one commit to examine what a commit ID is made of.

Tasks

  1. Find the commit that last changed README.md, and write its ID into a new file answer.txt. A short ID is enough.
  2. Save list.txt as it looked two commits before the newest one, in a new file old-list.txt. list.txt itself must stay unchanged.
  3. Make a new commit that has exactly the same files as its parent. Then confirm that the two commits have different IDs but the same tree.

Check yourself: Recall it

History is A, B, C, D, E, where E is newest and HEAD points to E. Which commit is HEAD~2?

  • D
  • C
  • B

Two commits have exactly the same files. Do they have the same ID?

  • Yes, the ID is computed from the files only
  • Not necessarily: parent, author, time and message are part of it too
  • No, IDs are random numbers

You rename notes.txt to ideas.txt without changing its content, and commit. What does the new commit store for ideas.txt?

  • A new blob with the new name
  • A line in the new tree that points to the old blob
  • Nothing, because only changes are stored

Review from the lesson on good commits: git log --oneline shows one line per commit. Which part of the commit message is that?

  • The subject line, the first line of the message
  • The body, because that is where the explanation is
  • A summary Git makes from the diff

Common misconceptions

  • “A commit ID is a random number or a counter.” It is a hash of the whole commit, including parent, author, time and message.
  • “HEAD~2 is the second commit.” The tilde counts back from HEAD, not from the start.
  • “Complete snapshots must take a lot of space.” An unchanged file points to the same blob as before.

Read more

The three object types

Git stores file content and history as objects in .git/objects. Each object's ID is a hash of a short header with its type and size, followed by its content.

  • A blob is the content of one file, with no name, date or permissions. The same content always gets the same ID, even in another repository. git hash-object computes the ID without storing anything:
$ printf 'Welcome\nA tiny demo project.\n' | git hash-object --stdin
5f6e829bc5a7fcde1c549bf31b1292e3762a75ec
  • A tree is a folder: a list of mode, type, ID and name. Subfolders are further trees.
$ git ls-tree HEAD
100644 blob a2f179b6e361876a40e3debec4ab5d59af100618    INSTALL.md
100644 blob 5f6e829bc5a7fcde1c549bf31b1292e3762a75ec    README.md
  • A commit points to one tree and zero, one or more parents, and stores the author, committer and message:
$ git cat-file -p HEAD
tree b1a2df414fdbe20c422e0f593dd0952bfb400b08
parent 2a59dab4dcabe5f8d025d1ab94d1015d680fc576
author Student <student@example.invalid> 1791875457 +0200
committer Student <student@example.invalid> 1791875457 +0200
Use npm ci in install guide

The number after the email is the time in seconds since 1970, with the time zone. The author wrote the change. The committer made this exact commit object. When a commit is copied, for example by rebase or cherry-pick, the author is kept, while the committer and the commit time are new.

git cat-file -t gives an object's type, -p prints it readably, and -s gives its size in bytes.

The blob IDs above are the same in your repository, because the content is the same. The commit IDs will differ, because the time is part of the commit.

Same files, new commit

A commit's ID covers the whole object. A commit with no file changes still gets a new ID:

$ git commit --allow-empty -m "Mark review point"
$ git rev-parse HEAD HEAD~1 'HEAD^{tree}' 'HEAD~1^{tree}'
f86942208ecf3829d56f9b045b009cbd2a4f2c73
b3706e8326424b35f7a5c8e7c8870730b728d667
b1a2df414fdbe20c422e0f593dd0952bfb400b08
b1a2df414fdbe20c422e0f593dd0952bfb400b08

Two different commits, one shared tree. REV^{tree} means the commit's tree.

Navigating: tilde, caret and colon

Notation Means
HEAD~1, HEAD^ The parent of HEAD, always the first parent
HEAD~3 Follow the first parent three times
HEAD~2:list.txt The file list.txt as it was in that commit
HEAD^{tree} The commit's tree

A merge commit has two parents. HEAD^2 for the second one comes in the lesson on three-way merges.

Some shells give these characters a meaning of their own. In zsh with the extendedglob option, which many setups turn on, ^ is a pattern character. In the Windows command prompt cmd, ^ is an escape character. Put quotes around such expressions, for example git rev-parse "HEAD^{tree}". Double quotes work in every common shell, including cmd.

Finding a specific change

Option Shows
--oneline One line per commit
--graph --all The graph for all branches
--stat / -p Files and line counts / the full diff
-3 Only the three newest
-- file Only commits that changed the file
-S text Commits that change how many times the text occurs
--grep=word Commits with the word in the message
--author=name, --since="2 weeks ago" Filter by author and time
--format='%h %an %ar %s' Your own format: short ID, author, relative time, subject
$ git log --oneline -- INSTALL.md
b3706e8 Use npm ci in install guide
3cd1670 Add install guide
$ git log --oneline -S "npm ci"
b3706e8 Use npm ci in install guide

git show REV shows one commit with its diff against the parent. git show --stat REV gives just the file overview, and git show REV:README.md prints an old file version. git diff --stat HEAD~2 HEAD compares any two snapshots.

Ranges: A..B

git log A..B shows commits reachable from B but not from A. It is a set of nodes in the graph, not a time interval. After the empty commit above:

$ git log --oneline HEAD~2..HEAD
f869422 Mark review point
b3706e8 Use npm ci in install guide

With branches, main..feature gives what feature has that main lacks. You will use this a lot with branches and remotes later.

Error messages

$ git show HEAD~9
fatal: ambiguous argument 'HEAD~9': unknown revision or path not in the working tree.
$ git show HEAD:nope.md
fatal: path 'nope.md' does not exist in 'HEAD'

The first means the history is shorter than nine steps, or Git cannot tell whether you meant a commit or a file name. The second means the file did not exist in that snapshot. Check the name with git ls-tree. A short ID must have at least four characters and be unambiguous. If it matches several objects, Git answers short object ID … is ambiguous and lists the candidates.

Further reading