Embercode
✕
Git and GitHubLesson 11 of 16

45 min read

Finding the bad commit: blame and bisect

Why this matters

A project can have hundreds of commits. A test passed last week and fails today. Someone asks two questions: who changed this line, and which change broke the test? Git can answer both, and it does not need you to read every commit by hand.

git blame answers the first question. For each line in a file, it shows the last commit that changed that line, along with the author and the date.

git bisect answers the second question. You give it one commit where the test passed and one where it fails. It then checks commits in the middle, one at a time, until it names the first commit where the test started to fail. With 500 commits in between, it needs about nine checks, not 500.

Both commands only read history. Neither one changes a commit. git bisect does change the files in your folder while it runs, so the lesson ends that run with a reset step that puts everything back.

This lesson assumes you can make commits and read the output of git log. Fixing a commit after you make it is covered in the earlier lesson on amending and restoring. The lessons on reset and revert and on rebasing come later in this module.

A practice repository

You need a repository with a history to try these on. The script below builds one. It creates a small calculator in Python, a test file, and six commits by three people (Ana, Ben and Cy). One of the commits, by Ben, quietly breaks the test.

Run it in an empty folder. Python 3 must be installed (python3 --version should print a version number).

mkdir blame-practice && cd blame-practice
git init -q -b main
 
git config user.name Ana
git config user.email ana@example.com
 
cat > calc.py <<'EOF'
def average(nums):
    return sum(nums) / len(nums)
 
 
def clamp(value, low, high):
    return max(low, min(value, high))
EOF
 
cat > test_calc.py <<'EOF'
from calc import average, clamp
 
assert average([2, 4]) == 3
assert clamp(5, 0, 10) == 5
assert clamp(-5, 0, 10) == 0
EOF
 
git add . && git commit -q -m "Add average and clamp"
 
printf '# Calculator\n\nSmall helpers.\n' > README.md
git add README.md && git commit -q -m "Add README"
 
printf '\n\ndef double(x):\n    return x * 2\n' >> calc.py
git commit -qam "Add double helper"
 
git config user.name Ben
git config user.email ben@example.com
sed -i 's|return sum(nums) / len(nums)|return sum(nums[1:]) / len(nums)|' calc.py
git commit -qam "Skip first value in average to speed it up"
 
git config user.name Ana
git config user.email ana@example.com
printf '\n\ndef square(x):\n    return x * x\n' >> calc.py
git commit -qam "Add square helper"
 
git config user.name Cy
git config user.email cy@example.com
sed -i 's/def clamp(value, low, high):/def clamp(value,  low,  high):/' calc.py
git commit -qam "Reformat clamp signature"

Check that the history is there and that the test fails at the newest commit:

git log --oneline
python3 test_calc.py

The test file has three checks. An earlier commit made the first one fail. The error looks like this:

Traceback (most recent call last):
  File "/home/ana/blame-practice/test_calc.py", line 3, in <module>
    assert average([2, 4]) == 3
           ^^^^^^^^^^^^^^^^^^^^
AssertionError

The traceback names the failing line. A failing assert means the check after it was false. Here the average of 2 and 4 is not 3. Your path will differ from the one shown, but the last two lines match.

The hashes in the output below are short commit IDs. Yours will be different, because each commit ID depends on its content, author and time. The commands work the same way.

git blame: who last changed a line

Start with git blame on the calculator file:

git blame calc.py

This is the output from the script above, with the commit IDs and dates as they came out on the machine that ran it:

^1d6a1d3 (Ana 2026-11-05 10:31:29 +0000  1) def average(nums):
4ee44867 (Ben 2026-11-05 10:31:29 +0000  2)     return sum(nums[1:]) / len(nums)
^1d6a1d3 (Ana 2026-11-05 10:31:29 +0000  3) 
^1d6a1d3 (Ana 2026-11-05 10:31:29 +0000  4) 
206f9fd0 (Cy  2026-11-05 10:31:29 +0000  5) def clamp(value,  low,  high):
^1d6a1d3 (Ana 2026-11-05 10:31:29 +0000  6)     return max(low, min(value, high))
63f393e9 (Ana 2026-11-05 10:31:29 +0000  7) 
63f393e9 (Ana 2026-11-05 10:31:29 +0000  8) 
63f393e9 (Ana 2026-11-05 10:31:29 +0000  9) def double(x):
63f393e9 (Ana 2026-11-05 10:31:29 +0000 10)     return x * 2
def63565 (Ana 2026-11-05 10:31:29 +0000 11) 
def63565 (Ana 2026-11-05 10:31:29 +0000 12) 
def63565 (Ana 2026-11-05 10:31:29 +0000 13) def square(x):
def63565 (Ana 2026-11-05 10:31:29 +0000 14)     return x * x

Each row is one line of the file. Read it from left to right:

  • The first column is the short commit ID that last changed that line.
  • The name in parentheses is the author of that commit.
  • Then comes the date and time of that commit, in the author's time zone.
  • The number is the line number in the file, and the rest is the text of the line.

A ^ in front of a commit ID marks a commit with no earlier commit to compare against. In practice that is the first commit of the history, the one that created the file. In the output above, ^1d6a1d3 is "Add average and clamp", so lines 1, 3, 4 and 6 have not changed since the first commit.

Line 2 belongs to Ben's commit. That is the line that changed sum(nums) into sum(nums[1:]). Blame says who last touched that line, and the commit message says why. To read the whole commit, use its ID:

git show 4ee4486

The output starts with the commit ID, the author and the date. Then comes the message, "Skip first value in average to speed it up", followed by the changed lines.

Blame for one line

When a file is long, you often want one line. Pass -L with a line number, or a range of line numbers:

git blame -L 2,2 calc.py
4ee44867 (Ben 2026-11-05 10:31:29 +0000 2)     return sum(nums[1:]) / len(nums)

-L also accepts a search. This finds the line that starts with def clamp and the line after it. The number after the + counts the start line too, so +2 gives two lines:

git blame -L '/def clamp/,+2' calc.py
206f9fd0 (Cy  2026-11-05 10:31:29 +0000 5) def clamp(value,  low,  high):
^1d6a1d3 (Ana 2026-11-05 10:31:29 +0000 6)     return max(low, min(value, high))

The quotes matter. Without them, the space in def clamp splits the text into two separate arguments.

Blame and formatting changes

Here is a limit of blame that catches many beginners. Cy's last commit changed only the spaces in the clamp signature. The logic did not change at all. Plain blame now says Cy wrote line 5, which is true in one sense but misleading in another. The person who wrote the logic was Ana.

Run blame with -w to ignore whitespace changes:

git blame -w -L 5,7 calc.py
^1d6a1d3 (Ana 2026-11-05 10:31:29 +0000 5) def clamp(value,  low,  high):
^1d6a1d3 (Ana 2026-11-05 10:31:29 +0000 6)     return max(low, min(value, high))
63f393e9 (Ana 2026-11-05 10:31:29 +0000 7) 

Now line 5 is credited to the first commit again. Use -w whenever you are looking for the author of the logic, and the file has had a formatting pass at some point. Formatting tools that rewrite a whole file are a common cause of this.

What blame does not tell you

Blame is a pointer, not a verdict. It shows the last commit that touched a line. That commit may have been a fix for an earlier bug, or a formatting change, or a rename. Read the commit message and the diff with git show before you decide anything. A line that Ben last changed may have been correct in the version Ben wrote, and broken by something that did not touch that line at all.

Blame also cannot name an author for code you have not committed. Add a line to calc.py without committing it, and blame reports it like this:

git blame -L 15,15 calc.py
00000000 (Not Committed Yet 2026-11-05 10:32:43 +0000 15) extra

The all-zero commit ID means the line exists only in your folder.

git bisect: finding the commit that broke a test

Blame works when you know which line is wrong. Often you know only that the test fails, and you have no idea which line is wrong. That is what bisect is for.

Bisect uses the same idea as the number-guessing game where someone thinks of a number between 1 and 100, and you guess by halving. You do not check each number. You check 50, and then 25 or 75, and so on. Each guess cuts the range in half.

Bisect does this with commits. You name one commit where the test passed (the good commit) and one where it fails (the bad commit). Git checks out the commit in the middle, you run the test, and you tell Git whether that commit is good or bad. Git then cuts the range in half again. The number of checks grows slowly. For 500 commits, the number of checks is nine, because 2 to the power of 9 is 512, which is more than 500.

In the practice repository there are six commits. The newest is bad, and the one five steps back, HEAD~5, is good. That is the commit "Add average and clamp".

A manual bisect

Start the bisect, then give Git the bad commit and the good commit:

git bisect start
git bisect bad HEAD
git bisect good HEAD~5

The output after the second line is:

status: waiting for good commit(s), bad commit known

After the third line, Git checks out a commit in the middle of the range and reports how many commits are left to test:

Bisecting: 2 revisions left to test after this (roughly 1 step)
[63f393e938d57c258c479286399661999e90d375] Add double helper

Git is now sitting on "Add double helper". Your folder shows the files as they were at that commit. Run the test:

python3 test_calc.py
echo $?

The test passes, and echo $? prints 0. Tell Git that this commit is good:

git bisect good
Bisecting: 0 revisions left to test after this (roughly 1 step)
[def63565e62d1243136dde6f6b6beae9c464b741] Add square helper

Run the test again. It fails and prints an AssertionError, and echo $? prints 1. Tell Git that this commit is bad:

git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4ee44867d9f9aba4c4beb88cfbb965b860a14028] Skip first value in average to speed it up

One commit is left. Test it, and it fails, so mark it bad:

python3 test_calc.py
git bisect bad
4ee44867d9f9aba4c4beb88cfbb965b860a14028 is the first bad commit
commit 4ee44867d9f9aba4c4beb88cfbb965b860a14028
Author: Ben <ben@example.com>
Date:   Thu Nov 5 10:31:29 2026 +0000
 
    Skip first value in average to speed it up
 
 calc.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

The first bad commit is Ben's. The line that changed is the one blame showed earlier. Bisect took three checks, and blame gave the same answer for the line.

Before you do anything else, end the bisect:

git bisect reset

This puts you back on the branch you started from, here main. Without this step, your folder stays on the old commit, and new commits made there would not be on any branch.

An automatic bisect

Testing by hand is fine for six commits. For a long history, let Git run the test for you:

git bisect start
git bisect bad HEAD
git bisect good HEAD~5
git bisect run python3 test_calc.py

git bisect run runs the command at each step. It reads the exit code of the command. Exit code 0 means the commit is good. Any exit code from 1 to 124, or from 126 to 127, means the commit is bad. Exit code 125 means "this commit cannot be tested, skip it". Exit codes 128 and above stop the whole run with an error. Python exits with 1 when an assert fails, which is why the test above can be used directly.

The output of the automatic run is long, because the test prints its traceback at each step. The lines that matter are the step lines, and the last one:

Bisecting: 2 revisions left to test after this (roughly 1 step)
[63f393e938d57c258c479286399661999e90d375] Add double helper
running 'python3' 'test_calc.py'
Bisecting: 0 revisions left to test after this (roughly 1 step)
[def63565e62d1243136dde6f6b6beae9c464b741] Add square helper
running 'python3' 'test_calc.py'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4ee44867d9f9aba4c4beb88cfbb965b860a14028] Skip first value in average to speed it up
running 'python3' 'test_calc.py'
4ee44867d9f9aba4c4beb88cfbb965b860a14028 is the first bad commit

Run git bisect reset afterwards, as before.

A check that is not a test

git bisect run accepts any command, not only a test runner. Use this when the thing you are looking for is not a failing test, for example a file that should no longer exist, or a function that returns the wrong value. Write a short script in a folder outside the repository, so that the script itself is not removed or changed by checkouts:

#!/bin/sh
grep -q "nums\[1:\]" calc.py && exit 1
exit 0

The script exits 1 (bad) when the text nums[1:] is in calc.py, and 0 (good) when it is not. Save it as check.sh in a folder such as ~/bisect-checks/, then run it with its full path:

git bisect start HEAD HEAD~5
git bisect run sh ~/bisect-checks/check.sh

Git reports the same commit, 4ee44867, as the first bad commit. The script checks for one piece of text, so it finds the commit that introduced that text, even if no test covers it. This script was run on the practice repository and produced that result.

Notice that the script exits with 1 for bad. Bisect uses the exit code, not the words printed. A script that prints "bad" and exits 0 is treated as good.

Reading the bisect log

At any point, git bisect log prints every mark you made so far:

git bisect log
# bad: [206f9fd0633dcd5637e73fc6d41334331c9c5e2b] Reformat clamp signature
# good: [1d6a1d3c0e6e6118bd1596dc304a6edb99782921] Add average and clamp
git bisect start 'HEAD' 'HEAD~5'
# good: [63f393e938d57c258c479286399661999e90d375] Add double helper
git bisect good 63f393e938d57c258c479286399661999e90d375
# bad: [def63565e62d1243136dde6f6b6beae9c464b741] Add square helper
git bisect bad def63565e62d1243136dde6f6b6beae9c464b741
# bad: [4ee44867d9f9aba4c4beb88cfbb965b860a14028] Skip first value in average to speed it up
git bisect bad 4ee44867d9f9aba4c4beb88cfbb965b860a14028
# first bad commit: [4ee44867d9f9aba4c4beb88cfbb965b860a14028] Skip first value in average to speed it up

Here is the log of a bisect that had one bad mark and one good mark. It was saved with git bisect log > log-live.txt:

git bisect start
# status: waiting for both good and bad commits
# bad: [206f9fd0633dcd5637e73fc6d41334331c9c5e2b] Reformat clamp signature
git bisect bad 206f9fd0633dcd5637e73fc6d41334331c9c5e2b
# status: waiting for good commit(s), bad commit known
# good: [1d6a1d3c0e6e6118bd1596dc304a6edb99782921] Add average and clamp
git bisect good 1d6a1d3c0e6e6118bd1596dc304a6edb99782921

Each line starting with git bisect is a command you ran, and each # line is a note Git wrote. git bisect replay log-live.txt runs the same marks again in order, which brings the bisect back to the same place. This helps when a bisect is too long for one sitting. Most of the time you will not need it.

Common mistakes

Marking a commit with the wrong word. Bisect trusts whatever you tell it. If the test passed on a commit and you typed git bisect bad, Git will narrow the range towards the wrong commit and report it with confidence. Here is what happened when a bisect was run with a mistake. The test passed at "Add double helper", but the bisect was told it was bad. Git then asked for "Add README", which passed, and it reported this:

63f393e938d57c258c479286399661999e90d375 is the first bad commit
commit 63f393e938d57c258c479286399661999e90d375
Author: Ana <ana@example.com>
Date:   Thu Nov 5 10:31:29 2026 +0000
 
    Add double helper
 
 calc.py | 4 ++++
 1 file changed, 4 insertions(+)

The real culprit is Ben's commit, which comes right after it. The wrong mark sent the search in the wrong direction. Before each mark, run the test and check the exit code with echo $?. Then decide good or bad from that number.

Forgetting git bisect reset. After a bisect ends, the folder stays on the commit Git picked. If you make a commit there, it is not on main, and a later git checkout main can make it look lost. Run git bisect reset as the last step, every time. Then check with git status that the branch name is shown.

Testing against a test that changed. Bisect checks each commit with the same command. If the test file itself was changed in the middle of the range, an old commit may fail because the test is new, not because the code is wrong. Either keep the test file the same across the range, or copy the newer test into the check script so each step uses the same test.

Using a dirty folder. Bisect checks out other commits. If you have uncommitted changes that touch the same files, Git stops with an error. This happened when a line was added to calc.py without a commit, then git bisect good HEAD~5 was run:

error: Your local changes to the following files would be overwritten by checkout:
	calc.py
Please commit your changes or stash them before you switch branches.
Aborting

Commit or stash your changes before you start the bisect. The lesson on stashing covers the second option.

Expecting blame to show the bug. Blame shows the last change to a line. A bug can be caused by a line that did not change at all, for example a function that now receives different input from another file. When blame shows an innocent-looking line, use bisect on the test instead of reading lines one by one.

Running the check on the wrong folder. git bisect run runs the command in the folder of the repository. A script that uses paths like ./data/input.txt will look for that file in the repository root. Write checks with paths that work from the root of the repository, or give the full path.

Practice

Exercise 1. In the practice repository, which commit last changed line 6, return max(low, min(value, high))? Use git blame, and then say whether that commit is the one that introduced the line.

Show the answer

The command is git blame -L 6,6 calc.py. The output shows commit 1d6a1d3, "Add average and clamp", by Ana. That commit is also the first commit in the history, which is why blame marks it with a ^. The line has never changed since it was written.

Exercise 2. Line 5 is the clamp signature, and Cy changed only its spacing. Use blame to find the author of the logic on line 6, ignoring whitespace changes. Name the commit and the author.

Show the answer

Run git blame -w -L 6,6 calc.py. The output shows commit 1d6a1d3, "Add average and clamp", written by Ana. Without -w, the answer would also be Ana for line 6, because line 6 did not change. The -w matters for line 5, where plain blame would credit Cy with the spacing change.

Exercise 3. Explain in your own words why bisect needs only about nine checks for 500 commits, and not 500 checks. Then say how many checks are needed for 1,000 commits.

Show the answer

Each check cuts the number of remaining candidate commits in half. After nine halvings, 500 candidates become fewer than one. Nine is the smallest number of halvings that reaches 1 from 500, because 2 to the power of 8 is 256, which is still too small to cover 500, and 2 to the power of 9 is 512, which covers it. For 1,000 commits, the answer is 10, because 2 to the power of 10 is 1,024.

Exercise 4. Write a bisect check script that finds the first commit where calc.py contains the text square. Use git bisect start HEAD HEAD~5, then run it. Which commit does Git report, and what is the exit code for a commit that does not contain the text?

Show the answer

A script that works is:

#!/bin/sh
grep -q "square" calc.py && exit 1
exit 0

Run it with git bisect run sh /full/path/to/check.sh. Git reports commit def63565, "Add square helper", because that is the first commit where the word appears. A commit that does not contain the text makes grep exit with 1, which the script turns into exit code 0. Exit code 0 means good in bisect, so older commits are good, and the first commit with the word is the first bad one.

Exercise 5. A check script reaches a commit where it cannot run at all, because the file it needs is missing. Which exit code should the script use for that commit, and what does Git do with it? What exit code would be wrong, and why?

Show the answer

The script should exit with 125. Git then skips that commit and picks another one to test. Exit code 1 would be wrong, because Git would record the commit as bad. That could send the search to an earlier commit that is not the real culprit. Use 125 only for commits that cannot be tested, not for commits that run and fail.

Summary

  • git blame FILE shows, for each line, the last commit that changed it, its author and its date.
  • Use -L to limit blame to a line range, and -w to ignore whitespace-only changes.
  • git bisect start, then git bisect bad and git bisect good, narrows the search by checking commits in the middle of the range.
  • git bisect run COMMAND runs a check at each step. Exit code 0 is good, 1 to 127 except 125 is bad, and 125 means skip.
  • Always finish with git bisect reset, and check the branch name afterwards.

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.