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
- Bruno's repository: our test bench
git logby default- Compact formats:
--onelineand--graph - Seeing what changed:
--statand--patch - Custom formats with
--pretty=format: - Filtering the history: count, dates, author and message
- Filtering by path and the "pickaxe" search (
-Sand-G) git show: inspecting a commit- How to refer to a commit
- Useful combinations for everyday work
- 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:
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.
git log by default
git log by defaultcommit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main) Author: Bruno Salas <[email protected]> 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 <[email protected]> Date: Thu Jul 30 17:42:08 2026 +0200 Apply style to completed tasks commit 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8b Author: Bruno Salas <[email protected]> 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.
--reverseflips it. AuthorandCommitterare different fields. Remember from the data model that a commit stores both. Normally they coincide andgit logshows only the first; they part company when somebody applies a patch written by another person, or on a rebase. To see both:
commit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main) Author: Bruno Salas <[email protected]> AuthorDate: Fri Jul 31 09:14:22 2026 +0200 Commit: Bruno Salas <[email protected]> CommitDate: Fri Jul 31 09:14:22 2026 +0200
git logonly shows the history reachable from where you are. By default it starts atHEADand follows the links to the parents. Commits living on other branches do not appear unless you ask for them explicitly or use--all.
- Compact formats:
--oneline and --graph
--oneline and --graph--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:
* 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:
--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.
- Seeing what changed:
--stat and --patch
--stat and --patch--stat: a per-file summary
commit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main) Author: Bruno Salas <[email protected]> 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 <[email protected]> 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 <[email protected]> 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
commit b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3 (HEAD -> main) Author: Bruno Salas <[email protected]> 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:
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.
- Custom formats with
--pretty=format:
--pretty=format:When none of the predefined formats fits, you can design your own with a template:
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 | [email protected] |
%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=shortb2e6d3f 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/2026An 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"* 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.
- 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
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" --onelineGit 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
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:
And the symmetrical filter for the committer:
By message
This is a regular expression too, and by default it is case-sensitive. To ignore case:
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 onceAnd watch out for this: when you combine filters of different kinds, they apply with a logical AND:
Commits by Bruno, from 30 July onwards, whose message contains "style". Just the one.
- Filtering by path and the "pickaxe" search (
-S and -G)
-S and -G)By path
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:
Combined with -p, it is the best way of understanding how a file came to be what it is:
--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.
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.
It shows the commit where that CSS declaration appeared, with its diff. Surgical searching.
To see it in more detail:
-G: the regular-expression variant
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-Sdoes 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 show: inspecting a commit
git show: inspecting a commitOnce you have located the commit you are after, git show displays it in full:
commit 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8b Author: Bruno Salas <[email protected]> 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:
And combined with -s and a format, git show is a way of extracting specific data:
- 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 stillAn 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:
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^^
# → 3d5b8e1c4a7f2d9b6e3c8a1f5d2b7e4c9a6f3d8bThe 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:
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 notThat second-to-last one is exactly what Bruno has waiting to be pushed:
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
- 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# 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 --onelineAnd 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:
Common Mistakes and Tips
- Confusing
--grepwith-S.--grepsearches the commit message;-Ssearches 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.cssworks when there is no ambiguity, butgit log mainwill readmainas a branch. Put--in whenever there is room for doubt. - Expecting
git logto show every branch. By default it only walks the history reachable fromHEAD. To see the lot,--all. - Misreading
a..branges. The left-hand end is not included.git log HEAD~3..HEADreturns three commits, not four. - Using
~when you want^. In a linear history they are interchangeable, but on a mergeHEAD^2(the second parent) andHEAD~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 -pby eye. If you are looking for where a particular piece of text appeared,-Shands it to you in one command instead of making you read a hundred diffs. - Not using
--followwhen 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
lgalias from section 5. You will use it daily and it makes the history far more readable. - Tip:
git shortlog -snis the quickest way of learning who works on a project you have just cloned. Andgit log --oneline -20gives 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=shortproduces 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:
- The last three commits, one line each.
- All of Ana's commits.
- Everything done between 23 and 27 July, both included.
- The commits that touched
styles.css. - The commits whose message contains "task", ignoring case.
- A listing formatted as
hash | short date | author | subject. - How many commits there are in total and how many each person has made.
- The contents of
README.mdas they stood at4e7f2a9.
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"- What does
git log --oneline -S "DEBUGGING"return? Explain why each commit appears and why one is missing. - What does
git log --oneline -G "DEBUGGING"return? Compare it with the previous result and explain the difference. - What does
git log --oneline --grep="DEBUGGING"return? And--grep="debug"? - Write the command that shows the diff of the exact commit where the trace was removed.
- Write, in one sentence, the rule that will let you choose between
-S,-Gand--grepin future.
Exercise 3: Navigating by reference
Working on the repository from exercise 2 (four commits, a linear history):
- Write four different ways of referring to the second commit in the history (the one that added the trace).
- Check with
git rev-parsethatHEAD~2andHEAD^^point at the same object. - Show the contents of
app.jsas they stood at the first commit, without modifying your working tree. - Show only the messages of the last two commits, with no diff.
- Use a range to list only the last two commits and explain why
HEAD~2..HEADreturns two and not three. - Find out which files each of the four commits modified, with a single command.
Solutions
Solution to Exercise 1
1. The last three:
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:
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:
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:
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:
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:
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:
8. The README at 4e7f2a9:
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:
Solution to Exercise 2
1. With -S:
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:
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:
None, because --grep searches the message, and no message contains the word in capitals.
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:
Or, more directly and more readably, locate the commit and show it:
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:
which shows the most recent commit in which the count of that string changed: precisely the removal.
5. The rule:
--grepsearches the message ("who said they did this?");-Sfinds when a piece of text appeared or disappeared in the code ("where did this line come from?");-Gfinds 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 oneAnd 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^^
# → 9b3e5d7f4a2c8e1b5d3f7a9c2e6b4d8f1a3c5e7bIdentical 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 <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:
Or, if you prefer the full view without the diff:
5. The range:
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:
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 logwalks the history backwards fromHEAD, in reverse chronological order, following the links to the parents.- The formats change completely what you see:
--onelinefor the working view,--graphfor the structure (essential as soon as there are branches),--statfor the reach of each change and-pfor the full diff. --pretty=format:lets you design your own output with placeholders such as%h,%an,%arand%s, and it is worth saving as an alias.- The filters turn
git loginto a search engine:-nby count,--since/--untilby date,--authorby person,--grepby message and-- <path>by file. Filters of different kinds combine with a logical AND. - The "pickaxe" search is the hidden gem:
-Sfinds when a piece of text appeared or disappeared in the code, and-Gwhich commits touched lines matching a pattern. Do not confuse either of them with--grep, which looks at the message. git showinspects 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. Rangesa..bexclude 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:
- Creating the repository (
git init) or joining an existing one (git clone). - Working through the edit → stage → commit cycle, with
git statusas your compass. - Choosing precisely what goes into each commit, right down to hunk level with
git add -p. - Reviewing the changes with
git diffbefore recording them. - Querying the past with
git logandgit 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
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
