Viewing History — git log, diff, and blame

8 min read

A Git repository is a database of changes. Three commands let you query it: git log lists commits, git diff shows what changed between two states, and git blame annotates a file with who-touched-each-line. These are the QA engineer's archaeology kit — the tools you reach for when a test starts failing and you need to know "what changed since yesterday?", "did this PR touch the search code?", or "who wrote this assertion and why?". This lesson covers the patterns you'll use a hundred times a week.

git log — the commit history

The bare command:

git log

Shows full-detail commits, newest first. Press q to quit. Useful but verbose. Most of the time you want the compact form:

git log --oneline
4c48901 Add cart fixture for discount-code edge cases
8f31320 Update search index after Chapter 8 added
dc57b17 Add Chapter 8 lessons: Capstone Project
ebc3006 Add Chapter 7 lessons: Error Handling & Debugging

One commit per line — short hash, message. That's the form you'll read most.

Useful flags

git log --oneline -10                   # last 10 commits
git log --oneline --graph               # ASCII branch graph
git log --oneline --graph --all         # graph including all branches
git log --author="Alice"                # only Alice's commits
git log --since="1 week ago"            # commits in the last week
git log --grep="flaky"                  # commits whose message contains "flaky"
git log -- cypress/e2e/login.spec.ts    # commits that touched one file
git log -p -- cypress/e2e/login.spec.ts # ...with the actual diff for each

The two dashes (--) before a path mean "everything after this is a file path, not a flag." Useful when filenames could be confused with options.

A QA workhorse: "what changed since the last release?"

git log v2.6.0..main --oneline

Two dots (..) mean "commits reachable from main but not from v2.6.0." Exactly the diff you'd put in your regression-test plan.

A real graph

Run git log --oneline --graph --all in a moderately busy repo and you see the branching shape:

*   9b3d2f1 Merge pull request #142 from acme/feature/payment-gateway
|\
| * 7a92f1e Add tests for declined-card flow
| * 3a7b2c1 Scaffold payment-gateway tests
* | e1a4c80 Update timeout config for slower staging
* | 5d8f3a2 Fix flaky search test
|/
* 4c48901 Add cart fixture for discount-code edge cases

The asterisks are commits, the lines are parent-child relationships, the diamond shape is a merge. Reading this in your terminal beats most GUI tools for raw situational awareness.

git diff — what's different

git diff answers "how do A and B compare?" The arguments determine A and B.

git diff

Shows changes in your working directory that aren't yet staged — i.e., what you've edited since the last git add.

git diff --staged

(also git diff --cached) — what's staged but not yet committed. Run this right before git commit to see exactly what's about to be saved.

Compare two branches:

git diff main..feature/checkout-tests

Compare two commits:

git diff abc1234 def5678

The last N commits:

git diff HEAD~3

HEAD is the current commit. HEAD~3 is "three commits before HEAD." So git diff HEAD~3 is "everything that's changed in my last three commits combined."

For a single file:

git diff HEAD~3 -- cypress/e2e/login.spec.ts

What the diff output means

diff --git a/cypress/e2e/login.spec.ts b/cypress/e2e/login.spec.ts
index a8f2c91..3c5e8a4 100644
--- a/cypress/e2e/login.spec.ts
+++ b/cypress/e2e/login.spec.ts
@@ -12,7 +12,7 @@ describe('login', () => {
     cy.get('input[name=email]').type('alice@example.com');
-    cy.get('input[name=password]').type('correcthorsebatterystaple');
+    cy.get('input[name=password]').type(Cypress.env('TEST_PASSWORD'));
     cy.get('button[type=submit]').click();
   });
 });
  • --- = the before version. +++ = the after version.
  • Lines starting with - were removed; lines starting with + were added.
  • @@ -12,7 +12,7 @@ is the hunk header — the line range in the old (-12,7) and new (+12,7) file, plus the function/section context.

Two minutes of diff-reading practice pays off forever; every PR you'll review is just diffs in a prettier wrapper.

git blame — who wrote each line, and when

git blame annotates every line of a file with the commit, author, and date that last changed it.

git blame cypress/e2e/login.spec.ts
^4c48901 (Vimal       2026-04-21 14:22) describe('login', () => {
^4c48901 (Vimal       2026-04-21 14:22)   it('lets a valid user in', () => {
3a7b2c1  (Alice       2026-04-29 09:11)     cy.visit('/login');
3a7b2c1  (Alice       2026-04-29 09:11)     cy.get('input[name=email]').type(...);
9b3d2f1  (Alice       2026-05-02 16:47)     cy.get('input[name=password]').type(Cypress.env('TEST_PASSWORD'));
^4c48901 (Vimal       2026-04-21 14:22)     cy.get('button[type=submit]').click();

For each line: short hash, author, date, the line itself. To dig into a specific change:

git show 9b3d2f1

You see the full commit — message, all files changed, the diff. Now you know not just who changed the password line, but why (assuming the commit message is decent).

git blame is mislabelled — it's almost always used not to assign fault but to find context. "Why does this assertion check for 200 OK instead of any 2xx?" is one git blame away from "the team had an incident in March; see commit 9b3d2f1."

A QA log+diff workflow

The tester's three-question routine when a test fails:

  1. What changed recently in this area? git log --oneline -10 -- cypress/e2e/checkout/
  2. What exactly did that PR change? git show <commit-hash> — read the diff carefully.
  3. Who wrote this specific assertion? git blame -L 80,90 cypress/e2e/checkout.spec.ts (only lines 80-90), then git show <commit> for context.

Three commands, two minutes, almost always enough to triage "is this a real bug, a flake, or a missed-update?"

What a graph view tells you at a glance

This kind of view (the same shape git log --all --oneline gives you in raw form) is how QA leads spot which areas got a lot of churn — the top of the list is also the top of the regression-test priority list for the next sprint.

git show — the deep dive on one commit

git show 4c48901

Prints the commit message, author, date, and the full diff of every file changed in that commit. The most common follow-up to git log or git blame. You'll alias it in your head as "tell me everything about this commit."

⚠️ Common mistakes

  • Reading raw git log and getting overwhelmed. Without --oneline, log is a wall of text. Default to --oneline --graph for situational awareness; reach for full log only when you need full bodies.
  • Confusing git diff and git diff --staged. They show different things — unstaged vs staged. Before every commit, run both: git diff (what's left out) and git diff --staged (what's going in). Surprises are usually one of these two.
  • Trusting git blame for blame. The line you're looking at may have moved through a refactor that left the original logic intact but the author "Alice." Use git log -p -- file to read the deeper history; blame is a starting point, not a verdict.

🎯 Practice task

Spend 25-30 minutes turning your repo's history into something you can read fluently. Use any active repo with at least 50-100 commits.

  1. Run git log --oneline -20. Read the last 20 commit messages. Are they descriptive enough that you could tell what happened? Note which ones aren't — your future self is the audience.
  2. Run git log --oneline --graph --all. Trace one merge commit visually — find the two parent lines that meet.
  3. Pick any file you've worked on. Run git log --oneline -- <path>. Pick a recent commit hash and run git show <hash> — read the full change.
  4. Run git blame <same-file> | head -30. Identify the most recent change. Run git show <its-commit> to see the surrounding context.
  5. Compare two branches: git diff main..feature/your-branch -- <file>. The diff is exactly what would merge if you opened a PR.
  6. Stretch: add a Git alias for the graph view. git config --global alias.lg "log --oneline --graph --all". Now git lg does it. Aliases are 30 seconds of setup that save you typing for the rest of your career.

The next lesson is the inverse of inspection — undoing things you wish hadn't happened.

// tip to track lessons you complete and pick up where you left off across devices.