Shattered Vales Studio Inc.

Reading History & Finding Bugs

A repository's history is documentation that's always up to date: it records not only what the code is, but how it got that way and why. This guide shows how to search that history to understand unfamiliar code, and how to pinpoint the exact commit that introduced a bug.

Step 1: Read the Log

git log lists commits, newest first. On its own it's verbose; these options make it useful. Press q to leave the log viewer, and / to search inside it.

git log --oneline --graph --decorate --all    # every branch as a compact graph
git log --oneline -20                         # last 20 commits
git log --stat                                # which files each commit touched
git log -p -- src/Inventory/ItemSorter.cs     # full changes to one file, commit by commit
git log --follow -- src/Inventory/ItemSorter.cs  # keep following the file across renames
git log --author="Alex" --since="2 weeks ago" # filter by author and date
git log --grep="inventory" -i                 # search commit messages, case-insensitive
git log Dev..feature/issue-42                 # commits on the feature branch that Dev doesn't have

Save the graph view as an alias so you can type git lg:

git config --global alias.lg "log --oneline --graph --decorate --all"

Step 2: Compare Commits and Branches

git show 3f2a9c1                              # one commit: message and changes
git show 3f2a9c1:src/Inventory/ItemSorter.cs  # a file exactly as it was at that commit
git diff Dev...feature/issue-42               # what the branch changed since it left Dev (what a PR shows)
git diff Dev feature/issue-42 --stat          # summary of differences between the two tips
git diff HEAD~3 -- src/                       # changes in src/ over the last 3 commits

Note the dots. Three dots (Dev...feature) compares the branch with the point where it split from Dev, so you see only the branch's own work. Two dots or a space compares the two latest commits directly, so changes made on Dev since then also show up, reversed.

Step 3: Find Out Why a Line Exists with Blame

git blame shows, for every line of a file, the last commit that changed it, who made it, and when. Despite the name, it's a tool for finding context, not someone to blame. Once you have the commit, read its message and pull request to learn why the code is the way it is.

git blame src/Inventory/ItemSorter.cs              # whole file
git blame -L 40,60 src/Inventory/ItemSorter.cs     # only lines 40 to 60
git blame -w -C src/Inventory/ItemSorter.cs        # ignore whitespace changes; detect code moved from other files
gh pr list --state merged --search "3f2a9c1"       # find the pull request that contained a commit

Large formatting commits make blame useless because they "touch" every line. List their hashes in a .git-blame-ignore-revs file at the repository root and Git (and GitHub's blame view) will skip them:

git config blame.ignoreRevsFile .git-blame-ignore-revs

Step 4: Search the Code and Its History

QuestionCommand
Where is this used today?git grep -n "CalculateTax"
Where was it used in an old version?git grep -n "CalculateTax" v1.2.0
When was this text added or removed?git log -S "MaxStackSize" --oneline
When did lines matching a pattern change?git log -G "Max(Stack|Slot)Size" --oneline
Which commits mention an issue?git log --grep="#42" --oneline

-S is called the pickaxe: it finds the commits where the number of occurrences of a string changed, which is exactly when something was introduced or deleted. It's the fastest way to answer "when did this constant change from 99 to 64?". Add -p to see the changes themselves.

Step 5: Find the Commit That Broke Something with Bisect

When something used to work and now doesn't, and you can't see why, git bisect finds the guilty commit by binary search. You name one commit where it's broken and one where it worked; Git checks out the commit halfway between, and you test it and report "good" or "bad". Each answer halves the range, so even 1,000 commits take only about 10 tests.

  1. Commit or stash your work so the working tree is clean.
  2. Find a commit or tag where it worked, such as the last release.
  3. Start bisecting and mark the two ends.
  4. Test the checked-out commit (build and run, or run the failing test) and mark it.
  5. Repeat until Git prints "<hash> is the first bad commit".
  6. End the session to return to your branch.
git bisect start
git bisect bad                   # the current commit is broken
git bisect good v1.3.0           # this release worked
# ...Git checks out a commit in the middle. Test it, then:
git bisect good                  # works here
git bisect bad                   # broken here
git bisect skip                  # can't test this one (e.g. doesn't build)
# ...repeat until it names the first bad commit, then:
git bisect reset                 # back to where you started

If a script can detect the bug (exit code 0 for good, non-zero for bad), Git can do the whole search for you. For example, with a single failing .NET test:

git bisect start HEAD v1.3.0
git bisect run dotnet test --filter "FullyQualifiedName~ItemSorterTests"
git bisect reset

For Unity projects, testing each step usually means letting the editor re-import and playing the scene. It's slower, but still far quicker than reading hundreds of commits.

Step 6: Turn History into Understanding

When you join a project or pick up an unfamiliar area, a few minutes with history goes a long way:

  1. Run git log --oneline -- <folder> to see how the area evolved and which commits mattered.
  2. Use git shortlog -sn -- <folder> to see who has worked on it most. They're the people to ask.
  3. Blame the confusing lines, then read the pull request discussion behind them.
  4. Write down what you learn. If the README or comments were missing it, fix them in your next pull request.