Guide contents

Lesson 12 of 14

Git Essentials and Recovery

Git records the history of a project as a chain of snapshots called commits. This lesson runs the commands that create, inspect, and combine commits, and the commands that undo mistakes. After it, a reader can build a history, resolve a merge conflict, undo a commit in three different ways, and find a lost commit.

Command Purpose
git init, git status Create a repository and show what changed
git add, git commit Stage changes and record them as a commit
git log, git diff, git diff --staged Read history and compare versions
git branch, git switch, git merge, git rebase Work on parallel lines of history and combine them
git stash, git restore Set changes aside or discard them
git reset, git revert, git reflog Move or undo commits and recover lost ones
git bisect, git blame Find the commit that caused a bug or a line
.gitignore Keep generated files out of the repository

All examples use one repository, shop, built step by step, so each example continues from the previous one. Commit ids (the 7-character hexadecimal names such as e08dc16) depend on the author, the time, and the content, so they differ on every machine. Commands that need an id in the text use the id from the reader's own output. The examples were run with Git 2.43 on Linux. Commands that edit files use sed -i in the GNU form. On macOS (BSD sed) write sed -i '' instead. Git commands themselves are identical on macOS.

Before the first commit, Git needs a name and an email address for the commit author:

Terminal
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main

The last line makes new repositories start on a branch called main.

Example 1: git init

git init turns a directory into a repository. It answers "how do I start tracking this project?".

Terminal
mkdir shop && cd shop
git init
ls -A
Output
Initialized empty Git repository in /tmp/claude-0/cmd-c4/git/shop/.git/
.git

git init creates a hidden directory named .git. That directory holds the whole history, the branches, and the settings. The files next to it, called the working tree, are the ones the reader edits. ls -A lists hidden names without . and .., which shows .git. The path in the first line is the absolute path of the new repository, so it differs on every machine. Deleting .git removes the history and leaves the files.

Example 2: git status

git status answers "what is the state of my files right now?". It is the command to run before and after almost every other Git command.

Terminal
git status
printf '# Shop\n\nA tiny cart.\n' > README.md
cat > Cart.java <<'EOF'
public class Cart {
    private int items;

    public void add() {
        items++;
    }
}
EOF
git status
Output
On branch main

No commits yet

nothing to commit (create/copy files and use "git add" to track)
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	Cart.java
	README.md

nothing added to commit but untracked files present (use "git add" to track)

The first status reports the branch (main), that there are no commits yet, and that the directory is empty. After the two files are created, the second status lists them under Untracked files: files Git sees but does not record. Each section of the output ends with a hint that names the command for the next step. The section titles are the three places a change can be: untracked or modified in the working tree, staged, or committed.

Example 3: git add

git add copies the current content of a file into the staging area (also called the index), the list of changes that the next commit will contain. It answers "which changes go into the next commit?".

Terminal
git add README.md
git status -s
git add Cart.java
git status
Output
A  README.md
?? Cart.java
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   Cart.java
	new file:   README.md

git status -s (short) prints one line per file. The two characters before the name are the status in two columns: the left column is the staging area, the right column is the working tree. A means added to the staging area and unchanged since. ?? means untracked. After the second add, the long status lists both files under Changes to be committed. git add . stages everything under the current directory, and git add -p stages a file piece by piece. The staging area exists so that one commit can contain a chosen subset of the changes made.

Example 4: git commit

git commit records the staged changes as a new snapshot with a message. It answers "how do I save this state in the history?".

Terminal
git commit -m "Add README and Cart"
git status
Output
[main e08dc16] Add README and Cart
 2 files changed, 10 insertions(+)
 create mode 100644 Cart.java
 create mode 100644 README.md
On branch main
nothing to commit, working tree clean

-m supplies the message on the command line. Without -m, Git opens the editor. The first output line gives the branch (main), the marker root-commit is shown only for the very first commit (in this run the id is e08dc16), and the message. The next lines count the files and lines changed. create mode 100644 records each new file with its permission bits (100644 is a regular, non-executable file). After the commit, status reports working tree clean: no staged and no modified files. git commit -a stages every modified tracked file before committing, and it does not include untracked files.

Example 5: git log --oneline --graph

git log lists commits from newest to oldest. It answers "what happened in this project, and in what order?". Two more commits give the log something to show.

Terminal
sed -i 's/items++;/items++;\n        System.out.println("items=" + items);/' Cart.java
git commit -am "Print item count"
printf '\nRun with: javac Cart.java\n' >> README.md
git commit -am "Document build"
git log
git log --oneline
git log --oneline --graph
git log --stat -1
Output
[main 6bd22fd] Print item count
 1 file changed, 1 insertion(+)
[main d5fe92f] Document build
 1 file changed, 2 insertions(+)
commit d5fe92fbab7d06397c6839b4b37584ead046c2c6
Author: Demo <demo@example.com>
Date:   Sat Oct 3 10:03:00 2026 +0000

    Document build

commit 6bd22fd511da32172d36eecdfd3a092a0807c9d9
Author: Demo <demo@example.com>
Date:   Sat Oct 3 10:02:00 2026 +0000

    Print item count

commit e08dc1628a8560838c7f05c0bcd9e500bd65281a
Author: Demo <demo@example.com>
Date:   Sat Oct 3 10:01:00 2026 +0000

    Add README and Cart
d5fe92f Document build
6bd22fd Print item count
e08dc16 Add README and Cart
* d5fe92f Document build
* 6bd22fd Print item count
* e08dc16 Add README and Cart
commit d5fe92fbab7d06397c6839b4b37584ead046c2c6
Author: Demo <demo@example.com>
Date:   Sat Oct 3 10:03:00 2026 +0000

    Document build

 README.md | 2 ++
 1 file changed, 2 insertions(+)

Plain git log prints the full 40-character id, the author, the date, and the message. The author and date columns come from the reader's configuration and clock. --oneline prints the short id and the first line of the message, one commit per line. --graph draws the shape of the history on the left, with one * per commit. The history is a straight line here, so the graph is a column of stars. The graph becomes useful after branches exist (examples 10 to 12). --stat adds the files each commit changed, and -1 limits the output to one commit. -n 5 or -5 shows the newest five.

Option Effect
--oneline Short id and subject on one line
--graph ASCII drawing of branches and merges
--all Include every branch, not only the current one
--stat Files changed per commit
-n N Show at most N commits
--author=Ana Only commits by matching authors

Example 6: git diff

git diff shows the lines that changed between the working tree and the staging area. It answers "what have I edited since the last add or commit?".

Terminal
sed -i 's/items=/count=/' Cart.java
git diff
printf 'extra line\n' >> README.md
git diff --stat
Output
diff --git a/Cart.java b/Cart.java
index 964cf28..51d03dd 100644
--- a/Cart.java
+++ b/Cart.java
@@ -3,6 +3,6 @@ public class Cart {
 
     public void add() {
         items++;
-        System.out.println("items=" + items);
+        System.out.println("count=" + items);
     }
 }
 Cart.java | 2 +-
 README.md | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

The first line names the two versions, a/Cart.java (old) and b/Cart.java (new). The index line lists the short ids of the old and new file content and the file mode. --- and +++ repeat the old and new names. The line starting with @@ is a hunk header: -3,6 means the old text starts at line 3 and spans 6 lines, +3,6 gives the same for the new text. Lines starting with - were removed, lines starting with + were added, and lines starting with a space are unchanged context. --stat replaces the full patch with a summary: one line per file and a total.

Example 7: git diff --staged

git diff --staged (also --cached) compares the staging area with the last commit. It answers "what exactly will the next commit contain?".

Terminal
git add Cart.java
git diff
echo "--- staged"
git diff --staged
git restore --staged Cart.java
git restore Cart.java README.md
git status -s
Output
diff --git a/README.md b/README.md
index 6d03f42..354b948 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,4 @@
 A tiny cart.
 
 Run with: javac Cart.java
+extra line
--- staged
diff --git a/Cart.java b/Cart.java
index 964cf28..51d03dd 100644
--- a/Cart.java
+++ b/Cart.java
@@ -3,6 +3,6 @@ public class Cart {
 
     public void add() {
         items++;
-        System.out.println("items=" + items);
+        System.out.println("count=" + items);
     }
 }

After git add Cart.java, plain git diff no longer shows the Cart.java change, because the working tree now matches the staging area. It still shows README.md, which is not staged. git diff --staged shows the Cart.java change that the next commit would record. The three-way picture is: git diff compares working tree to staging area, git diff --staged compares staging area to the last commit, and git diff HEAD compares working tree to the last commit. The last two commands discard these experiments (example 14 explains them), and the empty status -s output confirms the tree is clean.

Example 8: git branch

A branch is a movable name that points to a commit. git branch lists, creates, and deletes branches. It answers "which lines of work exist?".

Terminal
git branch
git branch discount
git branch
git branch -v
Output
* main
  discount
* main
  discount d5fe92f Document build
* main     d5fe92f Document build

The first git branch lists one branch, main, and the * marks the branch that is checked out. git branch discount creates a new name that points at the same commit as main, and does not switch to it. The second listing shows both names. -v adds the id and message of the commit each branch points to; both branches point to d5fe92f. Creating a branch copies one pointer, not any files, so it takes no noticeable time or space. git branch -d name deletes a branch that has been merged, and -D deletes it even when it has not been merged.

Example 9: git switch

git switch changes the checked-out branch, which updates the working tree to that branch's files. It answers "how do I move to another line of work?".

Terminal
git switch discount
git branch
cat > Discount.java <<'EOF'
public class Discount {
    public static int percent(int total) {
        return total >= 100 ? 10 : 0;
    }
}
EOF
git add Discount.java
git commit -m "Add Discount"
git switch main
ls
git switch -c hotfix-readme
git branch
git switch main
git branch -d hotfix-readme
Output
Switched to branch 'discount'
* discount
  main
[discount a4022cf] Add Discount
 1 file changed, 5 insertions(+)
 create mode 100644 Discount.java
Switched to branch 'main'
Cart.java
README.md
Switched to a new branch 'hotfix-readme'
  discount
* hotfix-readme
  main
Switched to branch 'main'
Deleted branch hotfix-readme (was d5fe92f).

After git switch discount the * moves to discount. The commit for Discount.java goes onto that branch only. Switching back to main makes Discount.java disappear from the directory: ls lists only Cart.java and README.md. Git rewrites the working tree to match the branch, so a file that exists only on another branch is not there. git switch -c name creates the branch and switches to it in one step. git switch - returns to the previous branch. Git refuses to switch when uncommitted changes would be overwritten; commit them or stash them first (example 13). git checkout is the older command that does the work of switch and restore.

Example 10: git merge

git merge combines the history of another branch into the current branch. It answers "how do I bring the finished work of a branch into main?". Here both branches have new commits, so Git creates a merge commit with two parents.

Terminal
printf 'License: MIT\n' >> README.md
git commit -am "Add license line"
git merge discount -m "Merge discount into main"
git log --oneline --graph
ls
Output
[main c026275] Add license line
 1 file changed, 1 insertion(+)
Merge made by the 'ort' strategy.
 Discount.java | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 Discount.java
*   3c8c551 Merge discount into main
|\  
| * a4022cf Add Discount
* | c026275 Add license line
|/  
* d5fe92f Document build
* 6bd22fd Print item count
* e08dc16 Add README and Cart
Cart.java
Discount.java
README.md

The commit on main makes the histories diverge from d5fe92f. git merge discount is run on main, the branch that receives the work. Merge made by the 'ort' strategy names the algorithm Git used (ort is the default). The graph shows the fork at d5fe92f: the left line is main, the right line is discount, and the top commit 3c8c551 joins them. After the merge, Discount.java is in the directory. -m sets the merge commit message. When main has no new commits of its own, Git instead moves the main pointer forward and creates no merge commit; the output then says Fast-forward. git merge --no-ff forces a merge commit, and --ff-only refuses to merge unless a fast-forward is possible.

Example 11: a merge conflict

A conflict happens when both branches changed the same lines. Git cannot choose, so it stops and asks. This example edits the same line of Cart.java on two branches.

Terminal
git switch -c tax
sed -i 's/items++;/items += 2;/' Cart.java
git commit -am "Count two per add"
git switch main
sed -i 's/items++;/items += 3;/' Cart.java
git commit -am "Count three per add"
git merge tax
git status
cat Cart.java
git diff --name-only --diff-filter=U
Output
Switched to a new branch 'tax'
[tax 694d96b] Count two per add
 1 file changed, 1 insertion(+), 1 deletion(-)
Switched to branch 'main'
[main cf11e41] Count three per add
 1 file changed, 1 insertion(+), 1 deletion(-)
Auto-merging Cart.java
CONFLICT (content): Merge conflict in Cart.java
Automatic merge failed; fix conflicts and then commit the result.
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   Cart.java

no changes added to commit (use "git add" and/or "git commit -a")
public class Cart {
    private int items;

    public void add() {
<<<<<<< HEAD
        items += 3;
=======
        items += 2;
>>>>>>> tax
        System.out.println("items=" + items);
    }
}
Cart.java

CONFLICT (content) names the file. The merge is paused, and git status lists Cart.java as both modified under Unmerged paths. Git wrote conflict markers into the file. The text between <<<<<<< HEAD and ======= is the version from the current branch (main). The text between ======= and >>>>>>> tax is the version from the branch being merged. The fix is to edit the file so it holds the intended text and no marker lines, stage it, and commit. git diff --name-only --diff-filter=U lists only the unmerged files. git merge --abort returns to the state before the merge.

The next commands resolve the conflict by keeping the value 2:

Terminal
cat > Cart.java <<'EOF'
public class Cart {
    private int items;

    public void add() {
        items += 2;
        System.out.println("items=" + items);
    }
}
EOF
git add Cart.java
git commit --no-edit
git log --oneline --graph
git branch -d tax
Output
[main ba65b1d] Merge branch 'tax'
*   ba65b1d Merge branch 'tax'
|\  
| * 694d96b Count two per add
* | cf11e41 Count three per add
|/  
*   3c8c551 Merge discount into main
Deleted branch tax (was 694d96b).

git add tells Git the file is resolved. git commit --no-edit accepts the message that Git prepared (Merge branch 'tax'). The log shows a second merge commit with both parents. (The output shows only the top six lines of the graph; the older commits are the ones from example 10.)

Example 12: git rebase

git rebase replays the commits of the current branch on top of another branch. It answers "how do I update my branch so its history is a straight line after main?". The commits get new ids because their parent changes.

Terminal
git switch -c search
cat > Search.java <<'EOF'
public class Search {
    public static boolean matches(String name, String q) {
        return name.toLowerCase().contains(q.toLowerCase());
    }
}
EOF
git add Search.java
git commit -m "Add Search"
git switch main
printf 'Contact: shop@example.com\n' >> README.md
git commit -am "Add contact line"
git log --oneline --graph --all
git switch search
git rebase main
git log --oneline --graph --all
git switch main
git merge search
git branch -d search
Output
Switched to a new branch 'search'
[search ad87389] Add Search
 1 file changed, 5 insertions(+)
 create mode 100644 Search.java
Switched to branch 'main'
[main a262680] Add contact line
 1 file changed, 1 insertion(+)
* a262680 Add contact line
| * ad87389 Add Search
|/  
*   ba65b1d Merge branch 'tax'
|\  
Switched to branch 'search'
Successfully rebased and updated refs/heads/search.
* 09ae871 Add Search
* a262680 Add contact line
*   ba65b1d Merge branch 'tax'
|\  
Switched to branch 'main'
Updating a262680..09ae871
Fast-forward
 Search.java | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 Search.java
Deleted branch search (was 09ae871).

(Both graphs are cut to their top lines; the older history is the one from example 11.) Before the rebase the graph forks: Add Search (ad87389) and Add contact line (a262680) both sit on ba65b1d. git rebase main, run on search, sets the branch's own commits aside, moves the branch to the tip of main, and applies them again. The result is a line: Add Search now has id 09ae871, because the commit has a new parent, so it is a new commit. (A terminal also shows a Rebasing (1/1) progress line before the success message.) The following git merge search on main is a fast-forward: main moves from a262680 to 09ae871 and no merge commit is created. The rule that follows from the new ids: rebase only commits that nobody else has pulled, because a rebase rewrites them. A conflict during a rebase stops it; fix the file, run git add, then git rebase --continue, or run git rebase --abort to cancel.

Example 13: git stash

git stash saves uncommitted changes on a stack and restores a clean tree. It answers "how do I switch tasks without committing half-finished work?".

Terminal
printf '// work in progress\n' >> Cart.java
git status -s
git stash push -m "wip cart"
git status -s
git stash list
git stash pop
git status -s
git stash list
git restore Cart.java
Output
 M Cart.java
Saved working directory and index state On main: wip cart
stash@{0}: On main: wip cart
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   Cart.java

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (0ecf1491b890c2a3030fe404c2068b8f455bc886)
 M Cart.java

M (space, then M) means modified in the working tree and not staged. git stash push -m "wip cart" stores the change under a label and resets the file to the last commit, so the next git status -s prints nothing. git stash list shows the stack: stash@{0} is the newest entry. git stash pop applies the newest entry back to the working tree and drops it from the stack (Dropped refs/stash@{0}, with the id of the stash commit). git stash apply applies an entry and keeps it. After pop, status -s shows M Cart.java again, and the empty stash list shows the stack is empty. The final git restore discards the demo line. Stash does not include untracked files unless -u is given.

Example 14: git restore

git restore puts a file back to a recorded version. It answers "how do I throw away an edit, or remove a file from the staging area?". It has two forms.

Terminal
printf 'oops\n' >> README.md
git status -s
git restore README.md
git status -s
echo "--- staged"
printf 'oops again\n' >> README.md
git add README.md
git status -s
git restore --staged README.md
git status -s
git restore README.md
Output
 M README.md
--- staged
M  README.md
 M README.md

git restore README.md replaces the working-tree file with the staged (or committed) version. The edit is deleted and cannot be recovered by Git, because it was never committed. After it, status -s prints nothing. The second form, git restore --staged README.md, moves the file out of the staging area and leaves the edit in the working tree: the status changes from M (staged) to M (modified, unstaged). git restore --source=HEAD~2 file restores a file from an older commit. git restore works on files, and it never moves a branch.

Example 15: git reset --soft

git reset moves the current branch to another commit. The three modes differ in what happens to the staging area and the working tree. --soft moves only the branch. It answers "how do I undo a commit but keep all its changes staged?".

Terminal
printf 'one\n' > Notes.txt
git add Notes.txt
git commit -m "Add notes file"
printf 'two\n' >> Notes.txt
git commit -am "Add note two"
git log --oneline -3
git reset --soft HEAD~1
git log --oneline -3
git status -s
git diff --staged
git commit -m "Add note two"
Output
[main 0bc7122] Add notes file
 1 file changed, 1 insertion(+)
 create mode 100644 Notes.txt
[main 04ae020] Add note two
 1 file changed, 1 insertion(+)
04ae020 Add note two
0bc7122 Add notes file
09ae871 Add Search
0bc7122 Add notes file
09ae871 Add Search
a262680 Add contact line
M  Notes.txt
diff --git a/Notes.txt b/Notes.txt
index 5626abf..814f4a4 100644
--- a/Notes.txt
+++ b/Notes.txt
@@ -1 +1,2 @@
 one
+two
[main f2d4a55] Add note two
 1 file changed, 1 insertion(+)

HEAD is the name for the current commit, and HEAD~1 is its parent. git reset --soft HEAD~1 moves main back one commit. The log no longer shows Add note two. The change from that commit is still staged: status -s prints M Notes.txt, and diff --staged shows the added line. The working tree is untouched. A new commit with the same content gets a new id (f2d4a55). Use this form to redo a commit message or to combine several commits into one.

Example 16: git reset --mixed

--mixed is the default mode. It moves the branch and clears the staging area, and keeps the working tree. It answers "how do I undo a commit and its staging, but keep the edits in the files?".

Terminal
printf 'three\n' >> Notes.txt
git commit -am "Add note three"
git reset --mixed HEAD~1
git log --oneline -2
git status -s
cat Notes.txt
git add Notes.txt
git commit -m "Add note three"
Output
[main 81b8baa] Add note three
 1 file changed, 1 insertion(+)
Unstaged changes after reset:
M	Notes.txt
f2d4a55 Add note two
0bc7122 Add notes file
 M Notes.txt
one
two
three
[main c727458] Add note three
 1 file changed, 1 insertion(+)

After the reset, the commit is gone from the log, and Notes.txt still holds three. The status M shows the change as modified and unstaged. Git prints Unstaged changes after reset followed by the files affected. git reset HEAD~1 without a mode does the same. The command git reset file (no commit) unstages a single file, the same effect as git restore --staged file.

Example 17: git reset --hard

--hard moves the branch, clears the staging area, and overwrites the working tree. It answers "how do I throw away a commit and its changes completely?". It destroys uncommitted work.

Terminal
printf 'four\n' >> Notes.txt
git commit -am "Add note four"
git log --oneline -3
cat Notes.txt
git reset --hard HEAD~1
git log --oneline -3
cat Notes.txt
Output
[main b704adc] Add note four
 1 file changed, 1 insertion(+)
b704adc Add note four
c727458 Add note three
f2d4a55 Add note two
one
two
three
four
HEAD is now at c727458 Add note three
c727458 Add note three
f2d4a55 Add note two
0bc7122 Add notes file
one
two
three

Before the reset, the file has four lines. After git reset --hard HEAD~1, the line four is gone from the file and the commit is gone from the log. HEAD is now at names the new current commit. Uncommitted edits are lost for good, because Git never saved them. A committed change is different: Git keeps the commit for some time, and example 19 recovers it. The three modes compare as follows.

Mode Branch moves Staging area Working tree
--soft yes kept (changes stay staged) kept
--mixed (default) yes reset to the target commit kept
--hard yes reset to the target commit overwritten

reset rewrites the history of the branch. On a branch that others have pulled, use git revert (next example).

Example 18: git revert

git revert creates a new commit that undoes an earlier commit. The earlier commit stays in the history. It answers "how do I undo a change that is already shared?".

Terminal
git revert HEAD --no-edit
git log --oneline -4
cat Notes.txt
Output
[main 7574d61] Revert "Add note three"
 1 file changed, 1 deletion(-)
7574d61 Revert "Add note three"
c727458 Add note three
f2d4a55 Add note two
0bc7122 Add notes file
one
two

git revert HEAD computes the opposite of the newest commit (Add note three) and commits it. The message is Revert "..." followed by the original subject, and --no-edit keeps it without opening the editor. The log still lists Add note three, and a new commit above it removes the line, so Notes.txt ends with one and two. Because nothing is rewritten, other people's copies stay valid. git revert <id> reverts any commit, not only the newest, and it can conflict like a merge.

Example 19: git reflog

The reflog records every position HEAD has had in this repository, including commits that no branch points to any more. It answers "which commit did I just lose, and what is its id?". It recovers the commit lost in example 17.

Terminal
git reflog -n 6
git log --oneline -2 b704adc
git branch rescued b704adc
git branch
git show rescued:Notes.txt
git log --oneline --graph --all -n 5
Output
3fdfe90 HEAD@{0}: revert: Revert "Add note three"
c727458 HEAD@{1}: reset: moving to HEAD~1
b704adc HEAD@{2}: commit: Add note four
c727458 HEAD@{3}: commit: Add note three
f2d4a55 HEAD@{4}: reset: moving to HEAD~1
81b8baa HEAD@{5}: commit: Add note three
b704adc Add note four
c727458 Add note three
  discount
* main
  rescued
one
two
three
four
* 3fdfe90 Revert "Add note three"
| * b704adc Add note four
|/  
* c727458 Add note three
* f2d4a55 Add note two
* 0bc7122 Add notes file

Each reflog line has the commit id, a position name (HEAD@{0} is the newest), and the action that moved HEAD. The line b704adc HEAD@{2}: commit: Add note four is the lost commit, and the next line up, reset: moving to HEAD~1, is the reset --hard that made it unreachable. The id b704adc in the commands is the id from this run, and a reader copies the id from their own reflog. git log --oneline -2 b704adc proves the commit still exists. git branch rescued b704adc puts a branch name on it, so the commit is reachable again. git show rescued:Notes.txt prints the file as it was in that commit, with four at the end. The graph shows rescued as a side branch. git reset --hard b704adc would instead move main itself back to the lost commit. The reflog is local and kept for at least 30 days by default for unreachable entries (the gc.reflogExpireUnreachable setting is 30 days).

Example 20: git bisect

git bisect finds the commit that introduced a bug by binary search over the history. It answers "which of these 8 commits broke the check?". This example uses a new repository, bisect-demo, whose eight commits each rewrite config.txt. From commit 5 on, the price is negative, and check.sh exits with status 1 when it sees a negative price.

Terminal
git log --oneline
./check.sh; echo "exit=$?"
git bisect start
git bisect bad
git bisect good HEAD~7
git bisect good
git bisect bad
git bisect bad
git bisect reset
git bisect start HEAD HEAD~7
git bisect run ./check.sh
git bisect reset
Output
7d30962 Change 8
8b519ea Change 7
3697ec5 Change 6
d93b65a Change 5
01f8f74 Change 4
d2dff29 Change 3
19b833c Change 2
c207e50 Change 1
exit=1
status: waiting for both good and bad commits
status: waiting for good commit(s), bad commit known
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[01f8f74c954c357b3afe411345fae77945c46826] Change 4
Bisecting: 1 revision left to test after this (roughly 1 step)
[3697ec595f284306829fb967ce75646c32a148f1] Change 6
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[d93b65aa94f0ebd566e193d8bc10780e95cf03b5] Change 5
d93b65aa94f0ebd566e193d8bc10780e95cf03b5 is the first bad commit
commit d93b65aa94f0ebd566e193d8bc10780e95cf03b5
Author: Demo <demo@example.com>
Date:   Sat Oct 3 11:05:00 2026 +0000

    Change 5

 config.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Previous HEAD position was d93b65a Change 5
Switched to branch 'main'
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[01f8f74c954c357b3afe411345fae77945c46826] Change 4
running './check.sh'
Bisecting: 1 revision left to test after this (roughly 1 step)
[3697ec595f284306829fb967ce75646c32a148f1] Change 6
running './check.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[d93b65aa94f0ebd566e193d8bc10780e95cf03b5] Change 5
running './check.sh'
d93b65aa94f0ebd566e193d8bc10780e95cf03b5 is the first bad commit
commit d93b65aa94f0ebd566e193d8bc10780e95cf03b5
Author: Demo <demo@example.com>
Date:   Sat Oct 3 11:05:00 2026 +0000

    Change 5

 config.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit
Previous HEAD position was d93b65a Change 5
Switched to branch 'main'

The check fails on the newest commit (exit=1). git bisect start begins a session. git bisect bad marks the current commit (Change 8) as bad, and git bisect good HEAD~7 marks Change 1 as good. Git then checks out the middle commit, Change 4, and says how many steps remain. The reader tests it and answers. Change 4 is good, so git bisect good moves the search to Change 6. That one is bad, so git bisect bad moves to Change 5, and a second bad ends the search: d93b65a is the first bad commit. Eight commits needed three checks, because each answer halves the range. git bisect reset returns to the original branch.

The second half automates the same search. git bisect start HEAD HEAD~7 gives the bad and good ends at once. git bisect run ./check.sh runs the script on each commit: exit status 0 means good, 1 to 127 (except 125) means bad, and 125 means skip this commit. The running lines show each test, and the final result is the same commit. The short-lived Previous HEAD position lines come from Git leaving the detached checkout at the end.

Example 21: git blame

git blame shows, for every line of a file, the commit and author that last changed it. It answers "who changed this line, and in which commit?". Back in shop, a commit by a second author, Ana, changes one printed label. The environment variables simulate a second author on one machine.

Terminal
sed -i 's/System.out.println("items=" + items);/System.out.println("total=" + items);/' Cart.java
GIT_AUTHOR_NAME=Ana GIT_AUTHOR_EMAIL=ana@example.com git commit -q -am "Rename printed label"
git blame Cart.java
git blame -L 4,6 Cart.java
Output
^e08dc16 (Demo 2026-10-03 10:01:00 +0000 1) public class Cart {
^e08dc16 (Demo 2026-10-03 10:01:00 +0000 2)     private int items;
^e08dc16 (Demo 2026-10-03 10:01:00 +0000 3) 
^e08dc16 (Demo 2026-10-03 10:01:00 +0000 4)     public void add() {
694d96be (Demo 2026-10-03 10:07:00 +0000 5)         items += 2;
6681a101 (Ana  2026-10-03 10:26:00 +0000 6)         System.out.println("total=" + items);
^e08dc16 (Demo 2026-10-03 10:01:00 +0000 7)     }
^e08dc16 (Demo 2026-10-03 10:01:00 +0000 8) }
^e08dc16 (Demo 2026-10-03 10:01:00 +0000 4)     public void add() {
694d96be (Demo 2026-10-03 10:07:00 +0000 5)         items += 2;
6681a101 (Ana  2026-10-03 10:26:00 +0000 6)         System.out.println("total=" + items);

Each output line has four parts: the commit id, the author and time in parentheses, the line number, and the line itself. The ^ before e08dc16 marks the root commit, and the id is one character shorter to make room for it. Line 5 comes from 694d96b (Count two per add), line 6 from Ana's commit, and the rest from the first commit. -L 4,6 limits the output to lines 4 to 6. Add -w to ignore whitespace changes, and -C to follow lines that were moved or copied from another file. Run git show <id> on an id from the output to read the full commit and its message.

Example 22: .gitignore

A .gitignore file lists patterns for files that Git must not track, such as compiled output and secrets. It answers "how do I keep generated files out of git status and out of commits?".

Terminal
mkdir build
printf 'junk' > build/Cart.class
printf 'junk' > Cart.class
printf 'secret=1\n' > .env
git status -s
printf '*.class\nbuild/\n.env\n' > .gitignore
git status -s
git check-ignore -v build/Cart.class .env
git status -s --ignored
git add .gitignore
git commit -m "Ignore build output"
Output
?? .env
?? Cart.class
?? build/
?? .gitignore
.gitignore:2:build/	build/Cart.class
.gitignore:3:.env	.env
?? .gitignore
!! .env
!! Cart.class
!! build/
[main e7d51df] Ignore build output
 1 file changed, 3 insertions(+)
 create mode 100644 .gitignore

Before the ignore file exists, the three new paths appear as untracked. After .gitignore is written, only .gitignore itself is untracked, so the generated files no longer clutter the status. Each line is a pattern. *.class matches a name ending in .class at any depth. build/ matches a directory named build. .env matches that file name. git check-ignore -v path explains why a path is ignored: it prints the file, the line number, the pattern, and the path. --ignored adds the ignored files to status, marked with !!. Committing .gitignore shares the rules with everyone who clones the repository. A pattern has no effect on a file that is already tracked: run git rm --cached file to stop tracking it and keep the file on disk. A pattern that starts with ! re-includes a name.

Interview points

  • The three areas are the working tree, the staging area (index), and the repository. git add moves changes into the staging area, and git commit moves them into the repository.
  • A branch is a pointer to a commit. Creating and switching branches rewrites no history.
  • git merge keeps the history as it happened and adds a merge commit when histories diverged. git rebase replays commits onto a new base, gives them new ids, and produces a linear history.
  • Rebase only commits that have not been shared, because it replaces commit ids.
  • reset --soft keeps changes staged, --mixed keeps them unstaged, and --hard discards them. revert adds a new commit that undoes an earlier one and is safe on shared branches.
  • git reflog lists previous positions of HEAD, so a commit lost to reset --hard can be found and given a branch name. It works only in the local repository.
  • A merge conflict marks the file with <<<<<<<, =======, and >>>>>>>. The steps are: edit, git add, git commit. git merge --abort cancels.
  • git bisect run <script> finds the first bad commit with a number of checks that grows with the logarithm of the commit count.
  • git blame names the last commit that changed each line. It does not name the first.
  • .gitignore does not affect files that are already tracked.

Summary

  • git init creates .git, which holds the whole history.
  • git status shows untracked, modified, and staged files, and -s prints two status columns.
  • git add stages content, and git commit -m records the staged content with a message.
  • git log --oneline --graph --all shows the shape of the history across branches.
  • git diff compares the working tree with the staging area, and git diff --staged compares the staging area with the last commit.
  • git branch lists and creates branches, and git switch and git switch -c change and create-and-change branches.
  • A merge makes a merge commit when both branches have new commits, and a fast-forward when only one does.
  • A conflict is resolved by editing the marked file, running git add, and committing.
  • git rebase main replays the branch commits on top of main with new ids.
  • git stash push -m saves uncommitted work, and git stash pop restores it.
  • git restore file discards edits, and git restore --staged file unstages.
  • git reset moves the branch with --soft, --mixed, or --hard, and git revert records the inverse of a commit.
  • git reflog finds commits that no branch reaches, and git bisect and git blame locate the commit behind a bug or a line.