# Dig Through History

Canonical: <https://mnixblog.netlify.app/dig-through-history/>

Blame, the Pickaxe, and the Reformat That Hid the Author

You are reading `checkout.js`, and one line puzzles you.

```js
return post(order, TIMEOUT_MS);
```


Why is there no retry here? You ask Git who wrote it, and `git blame` says Ravi, on April 22. His commit is called `Reformat with 2-space indent`. He did not write that line. He re-indented it.

Old code raises four questions: who wrote this line, when did this word appear, when did it vanish, and how did this function change. Git can answer all four, and it gets the right answer for that line:

```text
b226a4fd (Ada  2024-04-08 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);
```


## A history to dig through

Our scratch project is one file with seven commits from two made-up authors, Ada and Ravi. The output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash.

```console
$ git log --oneline
0963c24 Round tax
ab0f597 Reformat with 2-space indent
b226a4f Stop retrying orders
03b44d0 Give slow carriers more time
e3592b0 Add tax
83b088e Retry failed orders
8ea9a14 Add checkout total
```


Here is the file at the end:

```js
const TIMEOUT_MS = 60000;

function total(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

function send(order) {
  return post(order, TIMEOUT_MS);
}

function addTax(amount) {
  return Math.round(amount * 1.2);
}
```


<details>
<summary>Build the history yourself (Git Bash, optional)</summary>

Your hashes may differ from the ones printed here, because the dates use your time zone, and so will the time zone offsets in the blame output. The authors, messages and dates are set on purpose.

```bash
git init -b main digging && cd digging
c() {
  git add .
  GIT_AUTHOR_NAME="$2" GIT_AUTHOR_EMAIL="${2,,}@example.com" \
  GIT_COMMITTER_NAME="$2" GIT_COMMITTER_EMAIL="${2,,}@example.com" \
  GIT_AUTHOR_DATE="$3" GIT_COMMITTER_DATE="$3" git commit -m "$1"
}
cat > checkout.js <<'EOF'
const TIMEOUT_MS = 30000;

function total(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
}
EOF
c "Add checkout total" Ada 2024-02-05T10:00:00
cat > checkout.js <<'EOF'
const TIMEOUT_MS = 30000;
const RETRY_LIMIT = 3;

function total(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
}

function send(order) {
    for (let i = 0; i < RETRY_LIMIT; i++) {
        if (post(order, TIMEOUT_MS)) return true;
    }
    return false;
}
EOF
c "Retry failed orders" Ravi 2024-02-19T10:00:00
cat >> checkout.js <<'EOF'

function addTax(amount) {
    return amount * 1.2;
}
EOF
c "Add tax" Ada 2024-03-04T10:00:00
sed -i 's/TIMEOUT_MS = 30000/TIMEOUT_MS = 60000/' checkout.js
c "Give slow carriers more time" Ravi 2024-03-18T10:00:00
cat > checkout.js <<'EOF'
const TIMEOUT_MS = 60000;

function total(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
}

function send(order) {
    return post(order, TIMEOUT_MS);
}

function addTax(amount) {
    return amount * 1.2;
}
EOF
c "Stop retrying orders" Ada 2024-04-08T10:00:00
sed -i 's/^    /  /' checkout.js
c "Reformat with 2-space indent" Ravi 2024-04-22T10:00:00
sed -i 's/amount \* 1.2/Math.round(amount * 1.2)/' checkout.js
c "Round tax" Ada 2024-05-06T10:00:00
```


</details>

## Who changed this line

`git blame` prints each line of a file next to the commit that last changed it. The whole file is too much, so `-L` limits it to a range. Lines 7 to 9 hold `send`:

```console
$ git blame -L 7,9 checkout.js
83b088e7 (Ravi 2024-02-19 10:00:00 -0500 7) function send(order) {
ab0f597e (Ravi 2024-04-22 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);
83b088e7 (Ravi 2024-02-19 10:00:00 -0500 9) }
```


Each row has the start of the commit hash, the author, the date and the line number, then the line itself.

Line 8 belongs to `ab0f597`, the reformat. Blame names the **last** commit that touched the line. It does not look through whitespace or style changes unless we ask.

We can also give a function name instead of numbers:

```console
$ git blame -L :send checkout.js
83b088e7 (Ravi 2024-02-19 10:00:00 -0500  7) function send(order) {
ab0f597e (Ravi 2024-04-22 10:00:00 -0400  8)   return post(order, TIMEOUT_MS);
83b088e7 (Ravi 2024-02-19 10:00:00 -0500  9) }
e3592b05 (Ada  2024-03-04 10:00:00 -0500 10) 
```


Git found `send` by looking for lines that look like the start of a function, so this works best on languages it recognizes well. It printed lines 7 to 10, which includes the blank line after the function.

## Look past the reformat

There are two ways to get to the real author.

If the commit only changed whitespace, `-w` tells blame to ignore whitespace when it compares lines:

```console
$ git blame -w -L 7,9 checkout.js
83b088e7 (Ravi 2024-02-19 10:00:00 -0500 7) function send(order) {
b226a4fd (Ada  2024-04-08 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);
83b088e7 (Ravi 2024-02-19 10:00:00 -0500 9) }
```


If the commit changed more than whitespace, such as a code formatter that rewrote quotes and wrapped lines, name the commit with `--ignore-rev`. Blame then acts as if that commit never happened:

```console
$ git blame --ignore-rev ab0f597 -L 7,9 checkout.js
83b088e7 (Ravi 2024-02-19 10:00:00 -0500 7) function send(order) {
b226a4fd (Ada  2024-04-08 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);
83b088e7 (Ravi 2024-02-19 10:00:00 -0500 9) }
```


Line 8 is Ada's, from `Stop retrying orders`. That answers our question.

### Write it down for the team

Nobody wants to retype that. Put the commit in a file, one **full** hash per line, and point Git at it:

```bash
git log --format=%H --grep=Reformat > .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revs
```


Now plain `git blame -L 7,9 checkout.js` gives the answer above. The file takes full hashes. We first tried the short one, `ab0f597`, and Git answered:

```text
fatal: invalid object name: ab0f597
```


The manual says the same: "one unabbreviated object name per line." Lines beginning with `#` are ignored, so a comment saying what each commit was is a good habit.

Commit `.git-blame-ignore-revs` so everyone gets the list. The `git config` line is not shared by cloning, so each person runs it once.

## When did this string appear, or vanish

Blame is about lines that exist now. For a thing that is gone, we search the changes instead. `-S` takes a string and lists the commits that changed **how many times it appears** in the code:

```console
$ git log -S RETRY_LIMIT --oneline
b226a4f Stop retrying orders
83b088e Retry failed orders
```


The oldest line is the commit that added `RETRY_LIMIT`, and the newest is the one that removed it. Add `-p` to see the change itself. This is the removal:

```console
$ git log -S RETRY_LIMIT --oneline -p -1
b226a4f Stop retrying orders
diff --git a/checkout.js b/checkout.js
index e14e8d1..08bde68 100644
--- a/checkout.js
+++ b/checkout.js
@@ -1,15 +1,11 @@
 const TIMEOUT_MS = 60000;
-const RETRY_LIMIT = 3;
 
 function total(items) {
     return items.reduce((sum, item) => sum + item.price, 0);
 }
 
 function send(order) {
-    for (let i = 0; i < RETRY_LIMIT; i++) {
-        if (post(order, TIMEOUT_MS)) return true;
-    }
-    return false;
+    return post(order, TIMEOUT_MS);
 }
 
 function addTax(amount) {
```


That is the commit we wanted all along, found from a different direction.

### -S counts, -G reads

`TIMEOUT_MS` went from 30000 to 60000 in `03b44d0`. Search for it with `-S`:

```console
$ git log -S TIMEOUT_MS --oneline
83b088e Retry failed orders
8ea9a14 Add checkout total
```


`03b44d0` is missing. That commit changed the value, but the word `TIMEOUT_MS` appears the same number of times before and after, so `-S` does not count it. `-G` takes a regular expression and looks at the changed lines instead:

```console
$ git log -G "TIMEOUT_MS = [0-9]+" --oneline
03b44d0 Give slow carriers more time
8ea9a14 Add checkout total
```


There it is. Use `-S` when you want the commit where something was added or removed, and `-G` when you want any commit that touched a line matching a pattern. To search for a specific value, search for the value: `git log -S"TIMEOUT_MS = 30000" --oneline` printed `03b44d0` and `8ea9a14`.

## How did this function change

`git log -L` follows a range of lines, or a function, through every commit that touched it. It prints each change:

```console
$ git log -L :addTax:checkout.js
commit 0963c24c64bc87700452467c6015687f8d10df24
Author: Ada <ada@example.com>
Date:   Mon May 6 10:00:00 2024 -0400

    Round tax

diff --git a/checkout.js b/checkout.js
--- a/checkout.js
+++ b/checkout.js
@@ -11,3 +11,3 @@
 function addTax(amount) {
-  return amount * 1.2;
+  return Math.round(amount * 1.2);
 }

commit ab0f597e5e9e32f98e6e79732f6a79a6ab2ffa85
Author: Ravi <ravi@example.com>
Date:   Mon Apr 22 10:00:00 2024 -0400

    Reformat with 2-space indent
...
```


The third commit, `Add tax`, is the original. The middle one is the reformat again, which `-L` does not skip. That is the whole history of one function, in one command.

## Shape the output

The default `git log` is a lot of text. `--format` prints what we choose. These placeholders are the ones used most:

| Placeholder | Prints |
| --- | --- |
| `%h` | Short hash |
| `%H` | Full hash |
| `%an`, `%ae` | Author name and email |
| `%ad` | Author date, in the style `--date` sets |
| `%s` | Subject line |
| `%b` | Body of the message |
| `%d` | Branch and tag names |
| `%n` | A new line |

```console
$ git log --format="%h %an %ad %s" --date=short
0963c24 Ada 2024-05-06 Round tax
ab0f597 Ravi 2024-04-22 Reformat with 2-space indent
b226a4f Ada 2024-04-08 Stop retrying orders
03b44d0 Ravi 2024-03-18 Give slow carriers more time
e3592b0 Ada 2024-03-04 Add tax
83b088e Ravi 2024-02-19 Retry failed orders
8ea9a14 Ada 2024-02-05 Add checkout total
```


Filters narrow the list before it prints:

| Filter | Example | Result in our history |
| --- | --- | --- |
| `--since` | `git log --since=2024-04-01 --oneline` | `0963c24`, `ab0f597`, `b226a4f` |
| `--author` | `git log --author=Ravi --oneline` | `ab0f597`, `03b44d0`, `83b088e` |
| `--grep` | `git log --grep=tax -i --oneline` | `0963c24`, `e3592b0` |

`--grep` searches commit messages, and `-i` ignores case. For comparison, `-S` and `-G` search the changes. And `git log --oneline --graph --decorate` draws the branches. Ours is a straight line, so it is seven stars in a column.

## Try it

The whole hunt takes six commands.

**Practice terminal · dig through history**

Type these commands in order, in a practice folder:

1. Blame lines 7 to 9 of checkout.js. Try `git blame -L 7,9 checkout.js`. Line 8 is blamed on the reformat, `ab0f597`.
2. Blame the same lines, ignoring the reformat commit. Try `git blame --ignore-rev ab0f597 -L 7,9 checkout.js`. `git blame -w -L 7,9 checkout.js` works too, because that commit only changed whitespace. Line 8 now belongs to Ada, `b226a4f`.
3. Find the commits that added or removed RETRY_LIMIT. Try `git log -S RETRY_LIMIT --oneline`.
4. Find the commit that changed the value of TIMEOUT_MS, which `-S` misses. Try `git log -G "TIMEOUT_MS = [0-9]+" --oneline`.
5. Follow the addTax function through history. Try `git log -L :addTax:checkout.js`.
6. Print the hash, author, date and subject of every commit. Try `git log --format="%h %an %ad %s" --date=short`.

[Try the interactive Git practice terminal on the canonical article](https://mnixblog.netlify.app/dig-through-history/).


Two things worth trying: type `git log -S TIMEOUT_MS --oneline` at step 4 and see why it is not the answer, and ask for a hint at step 2.

**Challenge: one command, one line**

The retry loop ended with `return false;`, and `Stop retrying orders` removed it. Print who removed that line and on what date, as one line in the form `Ada 2024-04-08`. Use one `git log` command, and do not type a commit hash.

**Expected result:** exactly `Ada 2024-04-08`, and nothing else.

> **Tip:**
> `-S` lists the commit that added the string and the commit that removed it. Git prints the newest first.

<details>
<summary>Answer</summary>

```console
$ git log -S"return false;" -1 --format="%an %ad" --date=short
Ada 2024-04-08
```


Without `-1`, the same command also prints Ravi's `Retry failed orders`, the commit that added the line.

</details>

**Question:** `TIMEOUT_MS` changes from 30000 to 60000 in one commit. The word `TIMEOUT_MS` appears the same number of times before and after. Which command lists that commit?

1. `git log -G "TIMEOUT_MS = [0-9]+" --oneline`
2. `git log -S TIMEOUT_MS --oneline`
3. Both of them

**Answer:** 1. `git log -G "TIMEOUT_MS = [0-9]+" --oneline`

> **Explanation:** `-S` lists commits that change the number of times a string appears, and here it did not change. `-G` looks at the changed lines themselves.


**Question:** A `.git-blame-ignore-revs` file lists the commits that blame should skip. How must each commit be written?

1. As its full hash, one per line
2. As a short hash of seven characters
3. As the commit message

**Answer:** 1. As its full hash, one per line

> **Explanation:** Git's manual asks for one unabbreviated object name per line. In our run, the seven-character hash gave `fatal: invalid object name`.


**Question:** `git blame` shows a line next to a commit. Which commit is it?

1. The most recent one that changed that line
2. The first one that ever added that line
3. The one that last touched the file

**Answer:** 1. The most recent one that changed that line

> **Explanation:** Line 8 was written by Ada but blamed on Ravi's reformat, because his was the last commit to change it.


## What we learned

- `git blame -L 7,9 file` shows who last changed those lines. `-L :send` takes a function name.
- Blame names the last commit that touched a line, so a reformat hides the author.
- `git blame -w` ignores whitespace changes. `--ignore-rev <commit>` ignores a whole commit.
- `.git-blame-ignore-revs` lists full hashes, and `git config blame.ignoreRevsFile` points Git at it. The config is per clone.
- `git log -S text` finds commits that change how many times the text appears. `-G regex` finds commits whose changed lines match.
- `git log -L :function:file` shows every change to a function.
- `--format`, `--date`, `--since`, `--author` and `--grep` shape what `git log` prints.

Blame, the pickaxe and `-L` tell us which commit to look at. The commit message and the diff tell us why, and `git show b226a4f` prints both.
> **Portable fallback:** The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

[Join the discussion on the canonical article](https://mnixblog.netlify.app/dig-through-history/#ob-discussion)
