Why history matters

Every commit records a snapshot of the project, who made it, when, and why (see Commits for the basics). That makes the history a searchable record of every decision. When a router stops sending logs, a deployment script breaks, or a colleague asks "why is this timeout 30 seconds?", the answer is usually in the history — if you know how to search it.

The examples on this page use a small repository of network configuration files, maintained by Sam Rivera and Amara Okafor. The examples run with a fixed author and clock (2 March 2026, 09:00 Coordinated Universal Time, UTC), so the commit hashes on this page are exactly the ones you get when you type the same commands with the same name, email and dates. On your own machine the hashes will differ, because your name and the time are part of every commit.

Reading the log

The commit log is the list of commits reachable from where you are now, shown by git log. It starts at HEAD (your current commit) and follows the parent links backward, so the newest commit comes first. Each entry shows the full commit hash, the author, the author date and the complete message, with the body indented under the subject line:

git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server" -m "Clock drift broke log correlation during the last incident."
git log
commit 894563133ea529dcef999a91fa9a0e96a9e474ab
Author: Sam Rivera <sam@example.com>
Date:   Mon Mar 2 09:00:00 2026 +0000

    Add NTP server

    Clock drift broke log correlation during the last incident.

commit aedb4dc3b766e460a09b5adc1ab430d0a1f674e5
Author: Sam Rivera <sam@example.com>
Date:   Mon Mar 2 09:00:00 2026 +0000

    Add edge-01 base config

In a terminal, git log opens a pager (usually less) when the output is longer than the screen. Scroll with the arrow keys or the space bar, search with /, and press q to quit. git log -3 shows only the three newest commits.

Compact formats

The default format takes six lines per commit. For an overview, --oneline prints the abbreviated hash and the subject line only. When you need specific fields, --format takes placeholders: %h short hash, %an author name, %ad author date (made short with --date=short), %s subject. The example below builds a history of five commits on five different days, by two authors:

git init -q netops && cd netops
# commit DATE AUTHOR MESSAGE: commit everything staged, with a given date and author
commit() { GIT_AUTHOR_DATE="$1T10:00Z" GIT_COMMITTER_DATE="$1T10:00Z" git commit -q --author="$2" -m "$3"; }
printf 'hostname edge-01\ninterface ge-0/0/0\n' > router.cfg
git add router.cfg && commit 2026-03-02 "Sam Rivera <sam@example.com>" "Add edge-01 base config"
printf 'routers:\n  - edge-01\n' > inventory.yml
git add inventory.yml && commit 2026-03-03 "Amara Okafor <amara@example.com>" "Add inventory"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git add router.cfg && commit 2026-03-04 "Sam Rivera <sam@example.com>" "Add NTP server to edge-01"
printf 'logging host 192.0.2.20\n' >> router.cfg
git add router.cfg && commit 2026-03-05 "Amara Okafor <amara@example.com>" "Send edge-01 logs to syslog server"
printf '  - edge-02\n' >> inventory.yml
git add inventory.yml && commit 2026-03-06 "Sam Rivera <sam@example.com>" "Add edge-02 to inventory"
git log --oneline
git log --format='%h  %ad  %an  %s' --date=short
570c3d3 Add edge-02 to inventory
ef785b7 Send edge-01 logs to syslog server
b8fdd26 Add NTP server to edge-01
68d5221 Add inventory
8974a4d Add edge-01 base config
570c3d3  2026-03-06  Sam Rivera  Add edge-02 to inventory
ef785b7  2026-03-05  Amara Okafor  Send edge-01 logs to syslog server
b8fdd26  2026-03-04  Sam Rivera  Add NTP server to edge-01
68d5221  2026-03-03  Amara Okafor  Add inventory
8974a4d  2026-03-02  Sam Rivera  Add edge-01 base config

The small commit function at the top only exists to give each commit its own date and author, so the later filters have something to filter on. In daily work you just run git commit, and Git fills in your name and the current time. The abbreviated hash (seven characters here) is enough to name a commit in any command; Git makes it longer when a repository grows so large that seven characters become ambiguous.

Seeing what changed

--stat adds a summary of the files each commit changed and how many lines were added (+) or removed (-). -p (patch) adds the full diff. Both combine with every other option:

git init -q netops && cd netops
# commit DATE AUTHOR MESSAGE: commit everything staged, with a given date and author
commit() { GIT_AUTHOR_DATE="$1T10:00Z" GIT_COMMITTER_DATE="$1T10:00Z" git commit -q --author="$2" -m "$3"; }
printf 'hostname edge-01\ninterface ge-0/0/0\n' > router.cfg
git add router.cfg && commit 2026-03-02 "Sam Rivera <sam@example.com>" "Add edge-01 base config"
printf 'routers:\n  - edge-01\n' > inventory.yml
git add inventory.yml && commit 2026-03-03 "Amara Okafor <amara@example.com>" "Add inventory"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git add router.cfg && commit 2026-03-04 "Sam Rivera <sam@example.com>" "Add NTP server to edge-01"
printf 'logging host 192.0.2.20\n' >> router.cfg
git add router.cfg && commit 2026-03-05 "Amara Okafor <amara@example.com>" "Send edge-01 logs to syslog server"
printf '  - edge-02\n' >> inventory.yml
git add inventory.yml && commit 2026-03-06 "Sam Rivera <sam@example.com>" "Add edge-02 to inventory"
git log --stat -2
commit 570c3d3c3fa2b85ea36021a6b6f595ee6dec9d1f
Author: Sam Rivera <sam@example.com>
Date:   Fri Mar 6 10:00:00 2026 +0000

    Add edge-02 to inventory

 inventory.yml | 1 +
 1 file changed, 1 insertion(+)

commit ef785b78d7958f535bfb359e62ca3e7165aabc7e
Author: Amara Okafor <amara@example.com>
Date:   Thu Mar 5 10:00:00 2026 +0000

    Send edge-01 logs to syslog server

 router.cfg | 1 +
 1 file changed, 1 insertion(+)

git log -p -- router.cfg is a quick way to read the whole story of one file, change by change. How to read a diff is explained on Staging and Committing.

The commit graph

When branches have been merged, the history is not a straight line but a graph. --graph draws it in text: each * is a commit, and the lines on the left show which commits are on which line of work. Add --all to include every branch, not only the history of the current one. A common everyday combination is git log --oneline --graph --all; the next section shows its output.

Filtering the log

In a repository with thousands of commits, scrolling does not work. git log can filter on almost anything. The most useful filters, on the same five-commit history as above:

git init -q netops && cd netops
# commit DATE AUTHOR MESSAGE: commit everything staged, with a given date and author
commit() { GIT_AUTHOR_DATE="$1T10:00Z" GIT_COMMITTER_DATE="$1T10:00Z" git commit -q --author="$2" -m "$3"; }
printf 'hostname edge-01\ninterface ge-0/0/0\n' > router.cfg
git add router.cfg && commit 2026-03-02 "Sam Rivera <sam@example.com>" "Add edge-01 base config"
printf 'routers:\n  - edge-01\n' > inventory.yml
git add inventory.yml && commit 2026-03-03 "Amara Okafor <amara@example.com>" "Add inventory"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git add router.cfg && commit 2026-03-04 "Sam Rivera <sam@example.com>" "Add NTP server to edge-01"
printf 'logging host 192.0.2.20\n' >> router.cfg
git add router.cfg && commit 2026-03-05 "Amara Okafor <amara@example.com>" "Send edge-01 logs to syslog server"
printf '  - edge-02\n' >> inventory.yml
git add inventory.yml && commit 2026-03-06 "Sam Rivera <sam@example.com>" "Add edge-02 to inventory"
echo "== by Amara"
git log --oneline --author=Amara
echo "== since 5 March"
git log --oneline --since="2026-03-05 00:00"
echo "== touching inventory.yml"
git log --oneline -- inventory.yml
echo "== adding or removing 192.0.2.20"
git log --oneline -S "192.0.2.20"
== by Amara
ef785b7 Send edge-01 logs to syslog server
68d5221 Add inventory
== since 5 March
570c3d3 Add edge-02 to inventory
ef785b7 Send edge-01 logs to syslog server
== touching inventory.yml
570c3d3 Add edge-02 to inventory
68d5221 Add inventory
== adding or removing 192.0.2.20
ef785b7 Send edge-01 logs to syslog server
Option Keeps commits…
--author=Amara whose author name or email matches the pattern (a regular expression, so part of the name is enough)
--since="2026-03-05 00:00", --until=… made after or before a date; relative dates such as --since="2 weeks ago" work too. These filter on the committer date. Give a time as well: a date on its own (--since=2026-03-05) means that day at the current time of day, so the result changes depending on when you run the command.
-- inventory.yml that changed this file or folder. The -- separates paths from branch names and options.
-S "192.0.2.20" that changed how many times the text occurs: commits that added or removed it
-G "regex" whose diff has an added or removed line matching the regular expression
--grep="syslog" whose commit message matches the pattern

-S is nicknamed the pickaxe, because it digs up the commit where a piece of text appeared or disappeared. It is the fastest answer to "when did this IP address enter (or leave) our configs?" — even if the line was later moved or the file deleted. Filters combine: several conditions must all be true, so git log --author=Amara --since=2026-03-04 -- router.cfg shows only Amara's recent changes to router.cfg.

Relative references

Copying hashes is tedious. A relative commit reference names a commit by its position relative to another one, such as a branch name or HEAD:

  • HEAD~1 (or HEAD~) is the parent of HEAD; HEAD~2 its grandparent; main~3 three commits back from the tip of main. The tilde always follows the first parent.
  • HEAD^ (or HEAD^1) is also the first parent. The caret only differs on a merge commit, which has two parents: HEAD^2 is the second parent — the tip of the branch that was merged in.

In this example a branch acl-update is merged into main, and git rev-parse --short translates each reference into the commit it points to:

git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q -c acl-update
printf 'ip access-list MGMT\n' >> router.cfg
git commit -q -am "Add management ACL"
printf ' permit 198.51.100.0/24\n' >> router.cfg
git commit -q -am "Allow NOC subnet in ACL"
git switch -q main
printf 'logging host 192.0.2.20\n' > logging.cfg
git add logging.cfg && git commit -q -m "Add syslog server"
git merge -q --no-edit acl-update
git log --oneline --graph
for rev in HEAD HEAD~1 HEAD^ HEAD^2 HEAD~2 main~3; do
  echo "$rev = $(git rev-parse --short $rev)"
done
*   f7a9c98 Merge branch 'acl-update'
|\
| * e8cd318 Allow NOC subnet in ACL
| * 7664007 Add management ACL
* | dfa436e Add syslog server
|/
* 5a75fd4 Add NTP server
* aedb4dc Add edge-01 base config
HEAD = f7a9c98
HEAD~1 = dfa436e
HEAD^ = dfa436e
HEAD^2 = e8cd318
HEAD~2 = 5a75fd4
main~3 = aedb4dc

Read the graph from the top: the merge commit f7a9c98 has two parents. Its first parent, dfa436e, is the commit main was on when the merge happened; its second parent, e8cd318, is the tip of acl-update. HEAD~2 follows first parents twice and lands on 5a75fd4, skipping the branch entirely. References combine too: HEAD^2~1 is 7664007.

Revision ranges

A revision range selects a set of commits by reachability: which commits can be reached from one reference but not from another. Two notations cover most needs:

  • main..acl-update (two dots): commits reachable from acl-update but not from main. In plain words: "what does the branch have that main does not?" — exactly what a merge would bring in.
  • main...acl-update (three dots): commits reachable from either side but not from both — the symmetric difference. With --left-right, Git marks each commit with < (left side) or > (right side).

The same history as before, just before the merge:

git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q -c acl-update
printf 'ip access-list MGMT\n' >> router.cfg
git commit -q -am "Add management ACL"
printf ' permit 198.51.100.0/24\n' >> router.cfg
git commit -q -am "Allow NOC subnet in ACL"
git switch -q main
printf 'logging host 192.0.2.20\n' > logging.cfg
git add logging.cfg && git commit -q -m "Add syslog server"
git log --oneline main..acl-update
git log --oneline acl-update..main
git log --oneline --left-right main...acl-update
e8cd318 Allow NOC subnet in ACL
7664007 Add management ACL
dfa436e Add syslog server
< dfa436e Add syslog server
> e8cd318 Allow NOC subnet in ACL
> 7664007 Add management ACL

The range is about reachability, not about dates or the order of commits. Leaving out one side means HEAD: git log origin/main.. shows your local commits that are not on the server yet, and git log ..origin/main shows what others pushed that you have not merged. (Remote-tracking branches such as origin/main are explained on Remotes and Collaboration.)

Show and diff between commits

git show displays one object. For a commit, that is its log entry plus the patch it introduced (compared with its parent; for a merge commit Git shows a condensed combined diff, often empty — use git show --first-parent or -m to see more). With rev:path it prints a file exactly as it was in that commit, without changing your working tree. git diff with two commits compares their snapshots, however many commits lie between them:

git init -q netops && cd netops
# commit DATE AUTHOR MESSAGE: commit everything staged, with a given date and author
commit() { GIT_AUTHOR_DATE="$1T10:00Z" GIT_COMMITTER_DATE="$1T10:00Z" git commit -q --author="$2" -m "$3"; }
printf 'hostname edge-01\ninterface ge-0/0/0\n' > router.cfg
git add router.cfg && commit 2026-03-02 "Sam Rivera <sam@example.com>" "Add edge-01 base config"
printf 'routers:\n  - edge-01\n' > inventory.yml
git add inventory.yml && commit 2026-03-03 "Amara Okafor <amara@example.com>" "Add inventory"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git add router.cfg && commit 2026-03-04 "Sam Rivera <sam@example.com>" "Add NTP server to edge-01"
printf 'logging host 192.0.2.20\n' >> router.cfg
git add router.cfg && commit 2026-03-05 "Amara Okafor <amara@example.com>" "Send edge-01 logs to syslog server"
printf '  - edge-02\n' >> inventory.yml
git add inventory.yml && commit 2026-03-06 "Sam Rivera <sam@example.com>" "Add edge-02 to inventory"
git show HEAD~2
git show HEAD~3:router.cfg
git diff HEAD~4 HEAD -- router.cfg
git diff --stat HEAD~4 HEAD
commit b8fdd26006468d0b1a1a34fe0ef3a9a8a4808df2
Author: Sam Rivera <sam@example.com>
Date:   Wed Mar 4 10:00:00 2026 +0000

    Add NTP server to edge-01

diff --git a/router.cfg b/router.cfg
index 8f20631..90a65f0 100644
--- a/router.cfg
+++ b/router.cfg
@@ -1,2 +1,3 @@
 hostname edge-01
 interface ge-0/0/0
+ntp server 192.0.2.10
hostname edge-01
interface ge-0/0/0
diff --git a/router.cfg b/router.cfg
index 8f20631..83d087e 100644
--- a/router.cfg
+++ b/router.cfg
@@ -1,2 +1,4 @@
 hostname edge-01
 interface ge-0/0/0
+ntp server 192.0.2.10
+logging host 192.0.2.20
 inventory.yml | 3 +++
 router.cfg    | 2 ++
 2 files changed, 5 insertions(+)

The output contains four results: the commit HEAD~2 with its patch; router.cfg as it was three commits ago (two lines); the total change to router.cfg over four commits as one diff; and a --stat summary of everything that changed in that span. Useful variations:

  • git show --stat HEAD: only which files the last commit touched.
  • git show HEAD~3:router.cfg > router-old.cfg: save an old version next to the current one to compare or copy from.
  • git diff main acl-update -- router.cfg: how one file differs between two branches.
  • git diff --name-only HEAD~10 HEAD: only the file names, handy in scripts and pipelines.

Git blame: who changed this line?

Git blame (git blame) annotates every line of a file with the commit, author and date of the last change to that line. In the example below Sam creates the config, then Amara configures her own identity with git config and changes the syslog server:

git init -q netops && cd netops
printf 'hostname edge-01\nntp server 192.0.2.10\nlogging host 192.0.2.20\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
# Amara takes over: from now on her name goes into new commits
git config user.name "Amara Okafor"
git config user.email amara@example.com
sed -i 's/192.0.2.20/192.0.2.25/' router.cfg
GIT_AUTHOR_DATE=2026-03-04T14:30Z GIT_COMMITTER_DATE=2026-03-04T14:30Z git commit -q -am "Move syslog to new collector"
git blame router.cfg
git blame -L 3,3 --show-email router.cfg
^451c1e5 (Sam Rivera   2026-03-02 09:00:00 +0000 1) hostname edge-01
^451c1e5 (Sam Rivera   2026-03-02 09:00:00 +0000 2) ntp server 192.0.2.10
20197247 (Amara Okafor 2026-03-04 14:30:00 +0000 3) logging host 192.0.2.25
20197247 (<amara@example.com> 2026-03-04 14:30:00 +0000 3) logging host 192.0.2.25

Each line starts with the abbreviated hash of the commit that last changed it. The caret in ^451c1e5 marks a boundary commit — here the very first commit, which has no parent. Then come author, date, line number and the line itself. -L 3,3 limits the output to a line range, and --show-email shows emails instead of names. The natural next step is git show 20197247, to read the message and the full change.

Despite the name, blame is not about guilt: its purpose is to find the reason for a line through its commit message. That only works when commit messages explain why. Two options help when the last change was trivial: -w ignores whitespace-only changes, and --ignore-rev <hash> skips a commit such as a mass reformatting. GitHub shows the same information in the file view under Blame.

Git bisect: which commit broke it?

Something worked last month and is broken now, and two hundred commits lie in between. Git bisect (git bisect) finds the first bad commit by binary search: you name one bad commit and one good commit, Git checks out the commit halfway between them, you report whether it is good or bad, and the search space halves each round. Two hundred commits take about eight steps (28 = 256).

You can answer by hand with git bisect good and git bisect bad, but if a script can tell good from bad, git bisect run does the whole search on its own. The script must exit with status 0 for good, 1 to 127 (except 125) for bad, and 125 for "cannot test this commit, skip it". In this example the edge router's MTU (maximum transmission unit) must stay at 1500, and somewhere in eight commits it was changed:

git init -q netops && cd netops
printf 'hostname edge-01\nmtu 1500\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
# seven more commits; the fourth one quietly changes the MTU as well
for vlan in 10 20 30 40 50 60 70; do
  echo "vlan $vlan" >> router.cfg
  if [ $vlan = 40 ]; then sed -i 's/^mtu 1500$/mtu 9000/' router.cfg; fi
  git commit -q -am "Add VLAN $vlan"
done
git log --oneline

# the test: exit status 0 means good, 1 means bad
cat > ../check-mtu.sh <<'TEST'
#!/bin/sh
grep -q '^mtu 1500$' router.cfg
TEST
chmod +x ../check-mtu.sh

git bisect start HEAD HEAD~7     # bad commit first, then a known good one
git bisect run ../check-mtu.sh
fc53569 Add VLAN 70
0162864 Add VLAN 60
142d2c0 Add VLAN 50
e915737 Add VLAN 40
56cfdd5 Add VLAN 30
a0d2a3d Add VLAN 20
faab346 Add VLAN 10
8ec6448 Add edge-01 base config
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[56cfdd5f5ca5362c09bcb29d2993604d932657c6] Add VLAN 30
running  '../check-mtu.sh'
Bisecting: 1 revision left to test after this (roughly 1 step)
[142d2c0098246134f372364c56d892c951c882c3] Add VLAN 50
running  '../check-mtu.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[e915737ac3ba234c80c106b9acd7982a25c70bc9] Add VLAN 40
running  '../check-mtu.sh'
e915737ac3ba234c80c106b9acd7982a25c70bc9 is the first bad commit
commit e915737ac3ba234c80c106b9acd7982a25c70bc9
Author: Sam Rivera <sam@example.com>
Date:   Mon Mar 2 09:00:00 2026 +0000

    Add VLAN 40

 router.cfg | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
bisect found first bad commit

Git needed three test runs for seven candidate commits, and named e915737: the commit called "Add VLAN 40", which also changed the MTU — the kind of change a commit message hides and bisect finds. The --stat line confirms it: one line added and one changed. The test script lives outside the repository (../check-mtu.sh), so it stays the same while bisect checks out older commits.

During a bisect you are on a detached HEAD (see Branches). When you are done, always end the session; it returns you to the branch you started on:

git bisect reset     # end the session, go back to the original branch
git bisect log       # (during a session) the good/bad answers so far
git bisect skip      # (manual mode) this commit cannot be tested

Tips for finding things

Question Command
What happened recently? git log --oneline --graph --all -20
When did this text appear or disappear? git log -S "text" --oneline, then git show <hash>
Which commits mention a ticket number? git log --oneline --grep="CHG-1042"
What did this file look like last week? git log --oneline --until="1 week ago" -1 -- file, then git show <hash>:file
Who last changed this line, and why? git blame -L 10,20 file, then git show <hash>
What is on my branch that main does not have? git log --oneline main..
Where is the word in the current files? git grep -n "192.0.2.20" (searches tracked files only)
Which commit broke it? git bisect start <bad> <good>, then git bisect run ./test.sh

Good history makes all of this easier: clear commit messages that say why, ticket or change numbers in the message, and small commits that each do one thing. The search tools are only as useful as what was recorded.