So far we have always looked forwards: create the repository, stage changes, commit them, review the diff before recording it. This lesson turns the gaze around. We are going to query the past.

And this is where Git's real value lies. Storing versions is not much use if you cannot then answer questions like: when was this function introduced? Who touched styles.css last week, and why? In which commit did that line I could swear I wrote disappear? What was done on the project between Monday and Thursday?

git log answers all of them, but its default output is only the tip of the iceberg. It is an enormously configurable command: compact formats, graphs, per-file summaries, your own templates and a battery of filters that let you find one commit among thousands. In this lesson we will go through the whole of it, along with git show for inspecting a particular commit and the various ways of referring to a commit, which we have been using in passing (HEAD, HEAD~1) without fully explaining them.

Contents

  1. Bruno's repository: our test bench
  2. git log by default
  3. Compact formats: --oneline and --graph
  4. Seeing what changed: --stat and --patch
  5. Custom formats with --pretty=format:
  6. Filtering the history: count, dates, author and message
  7. Filtering by path and the "pickaxe" search (-S and -G)
  8. git show: inspecting a commit
  9. How to refer to a commit
  10. Useful combinations for everyday work

  1. Bruno's repository: our test bench

A few days have gone by since Bruno cloned the project. Ana already had four commits when he arrived, and since then Bruno has made three more in his own copy. This is the history sitting on his MacBook:

cd ~/Projects/task-manager
git log --oneline
b2e6d3f (HEAD -> main) Restore field focus after adding a task
7c1f4a9 Apply style to completed tasks
3d5b8e1 Add a pending tasks filter
c5d9b1e (origin/main, origin/HEAD) Document installation in the README
4e7f2a9 Add task deletion to the list
8b6d3c2 Add base styles for the list
1a4c8d6 Add initial task manager structure

Seven commits: Ana's four, which arrived with the clone, and the three Bruno has made locally. Notice (origin/main): it marks where the server stood when Bruno cloned. His three commits sit ahead of that mark because he has not pushed them yet; that is the business of module 4.

This mixed history will serve us well: it has two authors, different dates and changes to different files, which is exactly what we need to practise the filters.

  1. git log by default

git log
commit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main)
Author: Bruno Salas <bruno.salas@example.com>
Date:   Fri Jul 31 09:14:22 2026 +0200

    Restore field focus after adding a task

    After submitting the form the cursor was lost and you had to
    click the field again. The focus is now put back.

commit 7c1f4a9e2b6d8f3a5c1e7b9d4f2a6c8e3b5d1f7a
Author: Bruno Salas <bruno.salas@example.com>
Date:   Thu Jul 30 17:42:08 2026 +0200

    Apply style to completed tasks

commit 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8b
Author: Bruno Salas <bruno.salas@example.com>
Date:   Wed Jul 29 11:05:47 2026 +0200

    Add a pending tasks filter

(The output carries on; you leave the pager with q.)

Each entry has four elements:

Element What it is
commit <hash> The full 40-character SHA of the commit
Author Who wrote the change, with the user.name and user.email they configured
Date The authoring date, with time zone
Message Indented four spaces; first line and body

A couple of important points:

  • The order is reverse chronological: newest at the top. That is what you want 95% of the time. --reverse flips it.
  • Author and Committer are different fields. Remember from the data model that a commit stores both. Normally they coincide and git log shows only the first; they part company when somebody applies a patch written by another person, or on a rebase. To see both:
git log --pretty=fuller
commit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main)
Author:     Bruno Salas <bruno.salas@example.com>
AuthorDate: Fri Jul 31 09:14:22 2026 +0200
Commit:     Bruno Salas <bruno.salas@example.com>
CommitDate: Fri Jul 31 09:14:22 2026 +0200
  • git log only shows the history reachable from where you are. By default it starts at HEAD and follows the links to the parents. Commits living on other branches do not appear unless you ask for them explicitly or use --all.

  1. Compact formats: --oneline and --graph

--oneline

git log --oneline
b2e6d3f (HEAD -> main) Restore field focus after adding a task
7c1f4a9 Apply style to completed tasks
3d5b8e1 Add a pending tasks filter
c5d9b1e (origin/main, origin/HEAD) Document installation in the README
4e7f2a9 Add task deletion to the list
8b6d3c2 Add base styles for the list
1a4c8d6 Add initial task manager structure

One line per commit: abbreviated hash, the references pointing there and the first line of the message. It is the most used format, and it explains why that first line has to work on its own: in this view it is all you see.

--oneline is in fact shorthand for --pretty=oneline --abbrev-commit.

--graph

It draws the commit graph with text characters:

git log --oneline --graph
* b2e6d3f (HEAD -> main) Restore field focus after adding a task
* 7c1f4a9 Apply style to completed tasks
* 3d5b8e1 Add a pending tasks filter
* c5d9b1e (origin/main, origin/HEAD) Document installation in the README
* 4e7f2a9 Add task deletion to the list
* 8b6d3c2 Add base styles for the list
* 1a4c8d6 Add initial task manager structure

With a single line of development, the graph is a column of asterisks and adds little. Its value appears once there are branches and merges, where it shows the forks and the joining points:

*   9f3a2c1 (HEAD -> main) Merge the filters branch
|\
| * 5e8b1d4 Add the date filter
| * 2c7f9a3 Add the filter selector
* | 8d4e6b2 Fix the counter
|/
* c5d9b1e Document installation in the README

That is what the team's history will look like from module 3 onwards. To see the graph of all branches, not just the current one:

git log --oneline --graph --all --decorate

--decorate shows the references (branches, tags, HEAD) next to each commit. In modern Git it is on by default in the terminal, but it is worth knowing about.

  1. Seeing what changed: --stat and --patch

--stat: a per-file summary

git log --stat -3
commit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main)
Author: Bruno Salas <bruno.salas@example.com>
Date:   Fri Jul 31 09:14:22 2026 +0200

    Restore field focus after adding a task

 app.js | 2 ++
 1 file changed, 2 insertions(+)

commit 7c1f4a9e2b6d8f3a5c1e7b9d4f2a6c8e3b5d1f7a
Author: Bruno Salas <bruno.salas@example.com>
Date:   Thu Jul 30 17:42:08 2026 +0200

    Apply style to completed tasks

 styles.css | 7 +++++++
 1 file changed, 7 insertions(+)

commit 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8b
Author: Bruno Salas <bruno.salas@example.com>
Date:   Wed Jul 29 11:05:47 2026 +0200

    Add a pending tasks filter

 app.js     | 18 ++++++++++++++++--
 index.html |  6 ++++++
 2 files changed, 22 insertions(+), 2 deletions(-)

It is the most useful view for getting a quick sense of the size and reach of each change without reading the code. Variants:

git log --shortstat -3     # the totals line only
git log --name-only -3     # file names only
git log --name-status -3   # names with M/A/D/R

--patch (or -p): the full diff

git log -p -1
commit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main)
Author: Bruno Salas <bruno.salas@example.com>
Date:   Fri Jul 31 09:14:22 2026 +0200

    Restore field focus after adding a task

    After submitting the form the cursor was lost and you had to
    click the field again. The focus is now put back.

diff --git a/app.js b/app.js
index 3c9d4a2..5f8b2e1 100644
--- a/app.js
+++ b/app.js
@@ -58,5 +58,6 @@ document.querySelector('#new-task').addEventListener('submit', function (event)
   if (field.value.trim() !== '') {
     addTask(field.value.trim());
     field.value = '';
+    field.focus();
   }
 });

It shows the history with the diff of every commit, in the unified diff format you learned to read in the previous lesson. It is the most thorough way of reviewing work, and also the longest: always use it with a limit (-1, -5) or combined with a filter.

One particularly valuable combination:

git log -p -- app.js

It shows the complete evolution of a single file, commit by commit, with each one's diff. It is like watching a film of the file from birth onwards.

  1. Custom formats with --pretty=format:

When none of the predefined formats fits, you can design your own with a template:

git log --pretty=format:"%h · %an · %ar · %s"
b2e6d3f · Bruno Salas · 5 hours ago · Restore field focus after adding a task
7c1f4a9 · Bruno Salas · 20 hours ago · Apply style to completed tasks
3d5b8e1 · Bruno Salas · 2 days ago · Add a pending tasks filter
c5d9b1e · Ana Ferrer · 5 days ago · Document installation in the README
4e7f2a9 · Ana Ferrer · 7 days ago · Add task deletion to the list
8b6d3c2 · Ana Ferrer · 9 days ago · Add base styles for the list
1a4c8d6 · Ana Ferrer · 11 days ago · Add initial task manager structure

The placeholders start with %. These are the ones that actually get used:

Placeholder Content Example
%H Full hash b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3
%h Abbreviated hash b2e6d3f
%T / %t Tree hash (full / abbreviated) 9f4c2a8
%P / %p Parent hashes 7c1f4a9
%an Author name Bruno Salas
%ae Author e-mail bruno.salas@example.com
%ad Authoring date Fri Jul 31 09:14:22 2026 +0200
%ar Relative authoring date 5 hours ago
%as Short authoring date 2026-07-31
%cn / %ce / %cd / %cr The same for the committer
%s Subject (first line of the message) Restore field focus…
%b Message body
%d References (branches, tags) (HEAD -> main)
%D References without brackets HEAD -> main
%n Line break
%% A literal percent sign

Adding colour

The %C... placeholders control colour and make the output far more readable:

git log --pretty=format:"%C(yellow)%h%C(reset) %C(blue)%ad%C(reset) %C(green)%an%C(reset) %s" --date=short
b2e6d3f 2026-07-31 Bruno Salas Restore field focus after adding a task
7c1f4a9 2026-07-30 Bruno Salas Apply style to completed tasks
3d5b8e1 2026-07-29 Bruno Salas Add a pending tasks filter
c5d9b1e 2026-07-26 Ana Ferrer Document installation in the README

Available colours: red, green, yellow, blue, magenta, cyan, white, and %C(auto) to let Git decide. %C(reset) returns to the normal colour, and %C(bold …) gives bold.

Controlling the date format

git log --date=short --pretty=format:"%ad %s"      # 2026-07-31
git log --date=relative --pretty=format:"%ad %s"   # 5 hours ago
git log --date=iso --pretty=format:"%ad %s"        # 2026-07-31 09:14:22 +0200
git log --date=format:"%d/%m/%Y" --pretty=format:"%ad %s"   # 31/07/2026

An alias worth having

This template is a classic and deserves saving as an alias:

git config --global alias.lg "log --graph --pretty=format:'%C(yellow)%h%C(reset)%C(auto)%d%C(reset) %s %C(dim)(%ar) <%an>%C(reset)' --abbrev-commit"
git lg -5
* b2e6d3f (HEAD -> main) Restore field focus after adding a task (5 hours ago) <Bruno Salas>
* 7c1f4a9 Apply style to completed tasks (20 hours ago) <Bruno Salas>
* 3d5b8e1 Add a pending tasks filter (2 days ago) <Bruno Salas>
* c5d9b1e (origin/main, origin/HEAD) Document installation in the README (5 days ago) <Ana Ferrer>
* 4e7f2a9 Add task deletion to the list (7 days ago) <Ana Ferrer>

Aliases are stored in ~/.gitconfig through the mechanism we saw in Configuring Git, and they are covered in depth in Git Log and Aliases.

  1. Filtering the history: count, dates, author and message

On a two-year-old project, git log returns thousands of entries. The filters are what turn the command into a search tool.

By count

git log -3                 # the last 3
git log -n 3               # equivalent
git log --skip=3 -3        # the 3 before those

By date

git log --since="2026-07-29"
git log --after="2026-07-29"        # synonym for --since
git log --until="2026-07-30"
git log --before="2026-07-30"       # synonym for --until

# Combined: a time window
git log --since="2026-07-25" --until="2026-07-30" --oneline
3d5b8e1 Add a pending tasks filter
c5d9b1e Document installation in the README

Git also takes natural-language expressions, which is very convenient:

git log --since="2 weeks ago"
git log --since="yesterday"
git log --since="last monday"
git log --since="3 days ago" --until="1 day ago"

By author

git log --author="Ana" --oneline
c5d9b1e Document installation in the README
4e7f2a9 Add task deletion to the list
8b6d3c2 Add base styles for the list
1a4c8d6 Add initial task manager structure

The value is a regular expression matched against both the name and the e-mail, so a fragment is enough:

git log --author="example.com"         # the whole team
git log --author="Ana\|Bruno"          # either of the two

And the symmetrical filter for the committer:

git log --committer="Carla"

By message

git log --grep="filter" --oneline
3d5b8e1 Add a pending tasks filter

This is a regular expression too, and by default it is case-sensitive. To ignore case:

git log --grep="FILTER" -i --oneline

Several --grep options combine with a logical OR by default; --all-match turns them into an AND:

git log --grep="task" --grep="style" --oneline              # either of the two
git log --grep="task" --grep="style" --all-match --oneline  # both at once

And watch out for this: when you combine filters of different kinds, they apply with a logical AND:

git log --author="Bruno" --since="2026-07-30" --grep="style" --oneline
7c1f4a9 Apply style to completed tasks

Commits by Bruno, from 30 July onwards, whose message contains "style". Just the one.

  1. Filtering by path and the "pickaxe" search (-S and -G)

By path

git log --oneline -- styles.css
7c1f4a9 Apply style to completed tasks
8b6d3c2 Add base styles for the list

Only the commits that modified that file. As in git diff, the -- separates options from paths and is mandatory when the name could be confused with a branch.

It takes directories and patterns:

git log --oneline -- src/
git log --oneline -- "*.css"
git log --oneline -- app.js index.html

Combined with -p, it is the best way of understanding how a file came to be what it is:

git log -p --follow -- styles.css

--follow tracks the file across renames. Without it, the history would stop dead at the moment the file changed name. It only works with one file at a time.

The "pickaxe" search: -S

Here comes one of Git's most useful and least known abilities. -S finds the commits in which the number of occurrences of a string changed. In practice: when that text was introduced or removed.

git log -S "deleteTask" --oneline
4e7f2a9 Add task deletion to the list

A single commit: the one where the function appeared. If somebody deleted it later, that commit would show up too.

Compare it with --grep, which is a completely different thing:

Option Searches in Answers
--grep="X" The commit message Who wrote "X" in a message?
-S "X" The content of the files When did "X" appear or disappear from the code?
-G "X" The content, by regular expression Which commits touched lines matching "X"?

The star use case for -S is this: you find an odd line in the code, you want to know why it is there, and git blame only tells you who touched it last. With -S you find the original commit that introduced it, with its explanatory message.

git log -S "cursor: pointer" -p --oneline -- styles.css

It shows the commit where that CSS declaration appeared, with its diff. Surgical searching.

To see it in more detail:

git log -S "focus()" --pretty=format:"%h %ad %an — %s" --date=short
b2e6d3f 2026-07-31 Bruno Salas — Restore field focus after adding a task

-G: the regular-expression variant

git log -G "console\.log" --oneline

The difference from -S is subtle but important:

  • -S "text" finds the commits where the number of times that text appears changed. If you move a line elsewhere within the same file, the count does not change and -S does not find it.
  • -G "regex" finds every commit whose diff contains an added or removed line matching the expression. A move does show up.

A practical rule: -S for "when was this introduced?"; -G for "which commits touched something like this?".

Both accept --pickaxe-regex and combine with every other filter:

git log -S "tasks.filter" --author="Bruno" --since="1 week ago" -p

  1. git show: inspecting a commit

Once you have located the commit you are after, git show displays it in full:

git show 3d5b8e1
commit 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8b
Author: Bruno Salas <bruno.salas@example.com>
Date:   Wed Jul 29 11:05:47 2026 +0200

    Add a pending tasks filter

diff --git a/app.js b/app.js
index 5f8b2e1..8c3a7d9 100644
--- a/app.js
+++ b/app.js
@@ -30,6 +30,9 @@ function renderList() {
   const list = document.querySelector('#list');
   list.innerHTML = '';
-  for (const task of tasks) {
+  const visible = pendingOnly
+    ? tasks.filter(function (t) { return !t.done; })
+    : tasks;
+  for (const task of visible) {
...

Metadata and diff in a single view. It is equivalent to git log -p -1 <commit>, but more direct.

git show is more versatile than it looks, because it accepts any Git object:

# A commit
git show 3d5b8e1

# The current commit
git show
git show HEAD

# The per-file summary only
git show --stat 3d5b8e1

# The message only, with no diff
git show --no-patch 3d5b8e1
git show -s 3d5b8e1                # short form

# The names of the affected files only
git show --name-only 3d5b8e1

# A file AS IT WAS in that commit
git show 3d5b8e1:app.js

# The same file three commits ago
git show HEAD~3:styles.css

# A tree (the contents of a directory at that moment)
git show 3d5b8e1^{tree}

The <commit>:<path> syntax is especially handy. We already used it earlier in the module to check which version of a file had been recorded. It serves, for example, to recover an old version without disturbing anything else:

git show 8b6d3c2:styles.css > old-styles.css

And combined with -s and a format, git show is a way of extracting specific data:

git show -s --format="%an wrote this on %as" HEAD
# → Bruno Salas wrote this on 2026-07-31

  1. How to refer to a commit

We have been using HEAD, HEAD~1 and hashes without ever setting the notation out systematically. Let us close that gap, because it turns up in practically every Git command.

The hash

git show b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3    # full
git show b2e6d3f                                     # abbreviated
git show b2e6                                        # shorter still

An unambiguous prefix is all you need. Git requires at least 4 characters and uses 7 by default when displaying them. In very large repositories you may need a few more; if the prefix is ambiguous, Git tells you:

error: short object ID b2e is ambiguous

Symbolic references

Reference Means
HEAD The commit you are on right now
main The commit the main branch points at
origin/main The last known commit of main on the remote
v1.0 The commit tagged v1.0
@ A shorthand synonym for HEAD

The ~ and ^ operators

These two are the confusing pair, and the difference only matters when there are merges (commits with more than one parent):

Notation Means
HEAD~1 or HEAD~ The first parent of HEAD (the previous commit)
HEAD~2 The first parent of the first parent: two commits back
HEAD~n n commits back, always following the first parent
HEAD^1 or HEAD^ The first parent of HEAD — identical to HEAD~1
HEAD^2 The second parent of HEAD (only exists on a merge)
HEAD^^ The parent of the parent — identical to HEAD~2

In short:

  • ~ walks back through generations along the main line. ~3 = three steps back.
  • ^ chooses between the parents of one commit. ^2 = the second parent.

In a linear history like Bruno's, HEAD~2 and HEAD^^ are exactly the same thing:

git rev-parse HEAD~2
# → 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8b
git rev-parse HEAD^^
# → 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8b

The diagram makes it plain:

graph RL
    C7["b2e6d3f<br/>HEAD"] --> C6["7c1f4a9<br/>HEAD~1<br/>HEAD^"]
    C6 --> C5["3d5b8e1<br/>HEAD~2<br/>HEAD^^"]
    C5 --> C4["c5d9b1e<br/>HEAD~3"]
    C4 --> C3["4e7f2a9<br/>HEAD~4"]
    C3 --> C2["8b6d3c2<br/>HEAD~5"]
    C2 --> C1["1a4c8d6<br/>HEAD~6<br/>(root-commit)"]

The arrows point backwards because that is how Git works: each commit knows its parent, not its children. That is why walking the history into the past is easy and there is no simple notation for going forwards.

And once there are merges (module 3), ^ will come into its own:

graph RL
    M["9f3a2c1<br/>merge"] -->|"^1 (or ~1)"| A["8d4e6b2<br/>main"]
    M -->|"^2"| B["5e8b1d4<br/>filters branch"]

Ranges

Two dots define a range of commits:

git log 8b6d3c2..c5d9b1e --oneline
c5d9b1e Document installation in the README
4e7f2a9 Add task deletion to the list

Read it as "the commits reachable from c5d9b1e but not from 8b6d3c2". That is: everything after 8b6d3c2 up to and including c5d9b1e. Note that the left-hand end is left out.

Much used forms:

git log HEAD~3..HEAD --oneline      # the last 3 commits
git log origin/main..HEAD --oneline # what I have and the remote does not
git log HEAD..origin/main --oneline # what the remote has and I do not

That second-to-last one is exactly what Bruno has waiting to be pushed:

git log origin/main..HEAD --oneline
b2e6d3f Restore field focus after adding a task
7c1f4a9 Apply style to completed tasks
3d5b8e1 Add a pending tasks filter

His three local commits. All of this is developed in module 4.

Resolving any reference

git rev-parse translates any notation into a full hash, and it is the way to settle any doubt:

git rev-parse HEAD~3        # → c5d9b1e...
git rev-parse main          # → b2e6d3f...
git rev-parse --short HEAD  # → b2e6d3f

  1. Useful combinations for everyday work

A recipe book of queries that answer real questions:

# What has been done this week?
git log --since="1 week ago" --oneline

# What have I done this week? (for the Friday report)
git log --author="$(git config user.name)" --since="1 week ago" --oneline

# How many commits does the project have?
git rev-list --count HEAD

# Who has contributed, and how much?
git shortlog -sn
      4  Ana Ferrer
      3  Bruno Salas
# How did this file evolve?
git log -p --follow -- styles.css

# When was this string introduced?
git log -S "updateCounter" --oneline

# Which files does the project touch most?
git log --name-only --pretty=format: | sort | uniq -c | sort -rn | head

# The last commit that touched each file
git log --name-status -5

# One-line history with date and author
git log --pretty=format:"%h %as %an %s"

# Which commits are NOT on the remote?
git log origin/main..HEAD --oneline

And a practical warning: git log opens a pager. You move through it with the arrow keys or the space bar, search with /text and leave with q. If you prefer the output straight:

git --no-pager log --oneline -5

Common Mistakes and Tips

  • Confusing --grep with -S. --grep searches the commit message; -S searches the content of the files. They are completely different questions, and it is the commonest mistake when searching the history.
  • Forgetting the -- before a path. git log styles.css works when there is no ambiguity, but git log main will read main as a branch. Put -- in whenever there is room for doubt.
  • Expecting git log to show every branch. By default it only walks the history reachable from HEAD. To see the lot, --all.
  • Misreading a..b ranges. The left-hand end is not included. git log HEAD~3..HEAD returns three commits, not four.
  • Using ~ when you want ^. In a linear history they are interchangeable, but on a merge HEAD^2 (the second parent) and HEAD~2 (two generations back) point to different places.
  • Getting stuck in the pager. You leave with q. It is one of everybody's first frustrations with Git.
  • Scanning git log -p by eye. If you are looking for where a particular piece of text appeared, -S hands it to you in one command instead of making you read a hundred diffs.
  • Not using --follow when looking at the history of a renamed file. You will see a history that "starts" at the rename and wrongly conclude that the file is recent.
  • Tip: define the lg alias from section 5. You will use it daily and it makes the history far more readable.
  • Tip: git shortlog -sn is the quickest way of learning who works on a project you have just cloned. And git log --oneline -20 gives you the pulse of what is being done right now.
  • Tip: if you want to export the history for a report, --pretty=format: combined with --date=short produces lines any spreadsheet can process.

Exercises

Exercise 1: Queries on the task-manager history

Starting from Bruno's history as it appears in section 1:

b2e6d3f  Bruno  2026-07-31  Restore field focus after adding a task  (app.js)
7c1f4a9  Bruno  2026-07-30  Apply style to completed tasks           (styles.css)
3d5b8e1  Bruno  2026-07-29  Add a pending tasks filter               (app.js, index.html)
c5d9b1e  Ana    2026-07-26  Document installation in the README      (README.md)
4e7f2a9  Ana    2026-07-24  Add task deletion to the list            (app.js)
8b6d3c2  Ana    2026-07-22  Add base styles for the list             (styles.css, index.html)
1a4c8d6  Ana    2026-07-20  Add initial task manager structure       (all 4 files)

Write the exact command for each query and predict its output:

  1. The last three commits, one line each.
  2. All of Ana's commits.
  3. Everything done between 23 and 27 July, both included.
  4. The commits that touched styles.css.
  5. The commits whose message contains "task", ignoring case.
  6. A listing formatted as hash | short date | author | subject.
  7. How many commits there are in total and how many each person has made.
  8. The contents of README.md as they stood at 4e7f2a9.

Exercise 2: The pickaxe search

Create a repository with a history in which a line appears, moves and vanishes:

mkdir -p ~/practice/pickaxe && cd ~/practice/pickaxe
git init

printf 'function start() {\n  return true;\n}\n' > app.js
git add app.js && git commit -m "Add the start function"

printf 'function start() {\n  console.log("DEBUGGING: entering");\n  return true;\n}\n' > app.js
git commit -am "Add a debug trace"

printf 'function start() {\n  return true;\n}\n\nfunction end() {\n  console.log("DEBUGGING: entering");\n  return false;\n}\n' > app.js
git commit -am "Move the trace to the new end function"

printf 'function start() {\n  return true;\n}\n\nfunction end() {\n  return false;\n}\n' > app.js
git commit -am "Remove the debug trace"
  1. What does git log --oneline -S "DEBUGGING" return? Explain why each commit appears and why one is missing.
  2. What does git log --oneline -G "DEBUGGING" return? Compare it with the previous result and explain the difference.
  3. What does git log --oneline --grep="DEBUGGING" return? And --grep="debug"?
  4. Write the command that shows the diff of the exact commit where the trace was removed.
  5. Write, in one sentence, the rule that will let you choose between -S, -G and --grep in future.

Exercise 3: Navigating by reference

Working on the repository from exercise 2 (four commits, a linear history):

  1. Write four different ways of referring to the second commit in the history (the one that added the trace).
  2. Check with git rev-parse that HEAD~2 and HEAD^^ point at the same object.
  3. Show the contents of app.js as they stood at the first commit, without modifying your working tree.
  4. Show only the messages of the last two commits, with no diff.
  5. Use a range to list only the last two commits and explain why HEAD~2..HEAD returns two and not three.
  6. Find out which files each of the four commits modified, with a single command.

Solutions

Solution to Exercise 1

1. The last three:

git log --oneline -3
b2e6d3f (HEAD -> main) Restore field focus after adding a task
7c1f4a9 Apply style to completed tasks
3d5b8e1 Add a pending tasks filter

2. Ana's:

git log --author="Ana" --oneline
c5d9b1e Document installation in the README
4e7f2a9 Add task deletion to the list
8b6d3c2 Add base styles for the list
1a4c8d6 Add initial task manager structure

3. Between the 23rd and the 27th, both included:

git log --since="2026-07-23" --until="2026-07-28" --oneline
c5d9b1e Document installation in the README
4e7f2a9 Add task deletion to the list

Mind the detail: --until="2026-07-27" is read as 00:00 on that day, so it would exclude the commits made on the 27th itself. Since we want the whole of the 27th included, we write --until="2026-07-28". It is a classic mistake that makes commits on the last day of the range "disappear".

4. The ones that touched styles.css:

git log --oneline -- styles.css
7c1f4a9 Apply style to completed tasks
8b6d3c2 Add base styles for the list

A note: 1a4c8d6 created the file, so in a real history it would appear as well. The exercise states that the initial commit contained all four files, so the complete output would have three entries.

5. Messages containing "task", ignoring case:

git log --grep="task" -i --oneline
b2e6d3f Restore field focus after adding a task
7c1f4a9 Apply style to completed tasks
3d5b8e1 Add a pending tasks filter
4e7f2a9 Add task deletion to the list
1a4c8d6 Add initial task manager structure

Five of the seven. Left out are c5d9b1e ("Document installation in the README") and 8b6d3c2 ("Add base styles for the list"), whose messages do not contain the word.

6. A custom format:

git log --pretty=format:"%h | %as | %an | %s"
b2e6d3f | 2026-07-31 | Bruno Salas | Restore field focus after adding a task
7c1f4a9 | 2026-07-30 | Bruno Salas | Apply style to completed tasks
3d5b8e1 | 2026-07-29 | Bruno Salas | Add a pending tasks filter
c5d9b1e | 2026-07-26 | Ana Ferrer | Document installation in the README
4e7f2a9 | 2026-07-24 | Ana Ferrer | Add task deletion to the list
8b6d3c2 | 2026-07-22 | Ana Ferrer | Add base styles for the list
1a4c8d6 | 2026-07-20 | Ana Ferrer | Add initial task manager structure

7. Totals:

git rev-list --count HEAD
# → 7

git shortlog -sn
# →      4  Ana Ferrer
# →      3  Bruno Salas

8. The README at 4e7f2a9:

git show 4e7f2a9:README.md

It prints the contents of the file as they stood at that commit, without touching the working tree. If we wanted to save it separately:

git show 4e7f2a9:README.md > README-old.md

Solution to Exercise 2

1. With -S:

git log --oneline -S "DEBUGGING"
1f4c8a2 Remove the debug trace
9b3e5d7 Add a debug trace

Two commits. -S looks for changes in the number of occurrences of the string:

  • 9b3e5d7: went from 0 occurrences to 1. It changed → it appears.
  • 1f4c8a2: went from 1 to 0. It changed → it appears.

Missing is 7a2d6f9 ("Move the trace to the new end function"). And here is the nuance to grasp: in that commit the trace moved from start() to end(), so it still appears exactly once. Since the count does not vary (1 → 1), -S treats it as irrelevant and does not show it.

2. With -G:

git log --oneline -G "DEBUGGING"
1f4c8a2 Remove the debug trace
7a2d6f9 Move the trace to the new end function
9b3e5d7 Add a debug trace

Three commits. -G does not count occurrences: it checks whether the commit's diff contains an added or removed line matching the expression. In 7a2d6f9 there is one line with DEBUGGING removed and another added, so it does appear.

That is exactly the difference between the two options, and the exercise isolates it in its purest form.

3. With --grep:

git log --oneline --grep="DEBUGGING"
# → (no results)

None, because --grep searches the message, and no message contains the word in capitals.

git log --oneline --grep="debug"
1f4c8a2 Remove the debug trace
9b3e5d7 Add a debug trace

Two: the ones carrying that word in the message. That they coincide with the -S result is pure chance in this example; it happens because the messages describe well what they do.

4. The diff of the removal:

git log -p -S "DEBUGGING" --diff-filter=D --oneline

Or, more directly and more readably, locate the commit and show it:

git show 1f4c8a2
commit 1f4c8a2...
    Remove the debug trace

diff --git a/app.js b/app.js
@@ -4,4 +4,3 @@ function start() {
 function end() {
-  console.log("DEBUGGING: entering");
   return false;
 }

A robust way of chaining it, without knowing the hash in advance:

git show "$(git log --format=%H -S 'DEBUGGING' -1)"

which shows the most recent commit in which the count of that string changed: precisely the removal.

5. The rule:

--grep searches the message ("who said they did this?"); -S finds when a piece of text appeared or disappeared in the code ("where did this line come from?"); -G finds which commits touched lines matching a pattern, moves included ("who has been in here?").

Solution to Exercise 3

1. Four ways of referring to the second commit (with HEAD on the fourth):

git show 9b3e5d7      # 1. abbreviated hash
git show HEAD~2       # 2. two generations back
git show HEAD^^       # 3. the parent of the parent
git show HEAD~1^      # 4. the parent of the previous one

And there are more: the full hash, main~2, @~2… They all resolve to the same object.

2. Checking:

git rev-parse HEAD~2
# → 9b3e5d7f4a2c8e1b5d3f7a9c2e6b4d8f1a3c5e7b

git rev-parse HEAD^^
# → 9b3e5d7f4a2c8e1b5d3f7a9c2e6b4d8f1a3c5e7b

Identical hashes. In a linear history, ~n and n ^ symbols are equivalent, because each commit has a single parent and "pick the first parent" and "go back one generation" are the same operation. The difference only emerges on merges.

3. The file at the first commit:

git show HEAD~3:app.js
function start() {
  return true;
}
git status -s
# → (empty: the working tree has not been touched)

git show <commit>:<path> reads from the history and writes to standard output. It changes nothing, unlike git restore --source=..., which would overwrite the file on disk.

4. The messages of the last two only:

git log -2 --pretty=format:"%h %s"
1f4c8a2 Remove the debug trace
7a2d6f9 Move the trace to the new end function

Or, if you prefer the full view without the diff:

git log -2 --no-patch

5. The range:

git log HEAD~2..HEAD --oneline
1f4c8a2 Remove the debug trace
7a2d6f9 Move the trace to the new end function

Two commits, not three, because the left-hand end of the range is excluded. a..b means "everything reachable from b that is not reachable from a", and HEAD~2 is reachable from itself, so it is discarded. The way to read it is: "what has happened after HEAD~2".

If you wanted HEAD~2 included as well, you would have to write HEAD~3..HEAD.

6. Files modified by each commit:

git log --name-status --oneline
1f4c8a2 Remove the debug trace
M	app.js
7a2d6f9 Move the trace to the new end function
M	app.js
9b3e5d7 Add a debug trace
M	app.js
4c1e8b5 Add the start function
A	app.js

The last three commits modify app.js (M) and the first one adds it (A), which is consistent: it was the root-commit and the file did not exist before.

Conclusion

This lesson closes Git's basic cycle and module 2. Recapping what we covered here:

  • git log walks the history backwards from HEAD, in reverse chronological order, following the links to the parents.
  • The formats change completely what you see: --oneline for the working view, --graph for the structure (essential as soon as there are branches), --stat for the reach of each change and -p for the full diff.
  • --pretty=format: lets you design your own output with placeholders such as %h, %an, %ar and %s, and it is worth saving as an alias.
  • The filters turn git log into a search engine: -n by count, --since/--until by date, --author by person, --grep by message and -- <path> by file. Filters of different kinds combine with a logical AND.
  • The "pickaxe" search is the hidden gem: -S finds when a piece of text appeared or disappeared in the code, and -G which commits touched lines matching a pattern. Do not confuse either of them with --grep, which looks at the message.
  • git show inspects one particular commit — metadata and diff — and with the <commit>:<path> syntax it recovers any file exactly as it stood at any moment in the past.
  • Referring to a commit can be done by hash (full or abbreviated), by symbolic reference (HEAD, main, origin/main, a tag) or by relative navigation: ~ goes back generations and ^ chooses between parents. Ranges a..b exclude the left-hand end.

What you have taken from module 2

You have been through the complete cycle of working with Git on your own:

  1. Creating the repository (git init) or joining an existing one (git clone).
  2. Working through the edit → stage → commit cycle, with git status as your compass.
  3. Choosing precisely what goes into each commit, right down to hunk level with git add -p.
  4. Reviewing the changes with git diff before recording them.
  5. Querying the past with git log and git show.

With this you can already use Git productively on any project of your own. But there is a problem waiting.

What comes next: the team starts treading on each other's toes

Look at the real situation Ana and Bruno are in right now. Both of them have been working on main, each on their own laptop, with no coordination. Ana has added the task counter and the marking of completed tasks; Bruno has added the pending filter and touched the same files. Neither has seen the other's work.

When they try to bring all of that together, they will find two different versions of app.js that have evolved separately from the same starting point. And this is with only two people and a week's work: once Carla joins from Windows and there are three simultaneous lines of development, everybody working on main will be untenable.

There is a subtler problem too. Right now, if Ana wants to try a risky idea, she has nowhere to do it: anything she commits goes straight into the project's only line. And if a critical bug turns up halfway through an urgent task, she has to abandon what she is doing or commit it half finished.

The answer to all of this is branches, and they are the feature that made Git the industry standard. In module 3: Branching and Merging we will see what a branch really is (spoiler: you already know, it is a file with a hash inside, as we saw when creating the first commit), how to create them and move between them, how to merge several people's work, what strategies Git uses to do so, how to resolve conflicts when two people touch the same line, and how to keep a repository with many live branches in order.

Everything you have learned in this module — the three areas, the staging area, the diff, the history — remains valid exactly as it is. It is just that from now on there will be more than one line of work at a time.

Mastering Git: From Beginner to Advanced

Module 1: Introduction to Git

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved