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:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch mainThe 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?".
mkdir shop && cd shop
git init
ls -AInitialized empty Git repository in /tmp/claude-0/cmd-c4/git/shop/.git/
.gitgit 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.
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 statusOn 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?".
git add README.md
git status -s
git add Cart.java
git statusA 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?".
git commit -m "Add README and Cart"
git status[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.
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[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?".
sed -i 's/items=/count=/' Cart.java
git diff
printf 'extra line\n' >> README.md
git diff --statdiff --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?".
git add Cart.java
git diff
echo "--- staged"
git diff --staged
git restore --staged Cart.java
git restore Cart.java README.md
git status -sdiff --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?".
git branch
git branch discount
git branch
git branch -v* main
discount
* main
discount d5fe92f Document build
* main d5fe92f Document buildThe 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?".
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-readmeSwitched 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.
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[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.mdThe 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.
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=USwitched 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.javaCONFLICT (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:
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[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.
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 searchSwitched 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?".
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 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.
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 M README.md
--- staged
M README.md
M README.mdgit 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?".
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"[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?".
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"[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.
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[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
threeBefore 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?".
git revert HEAD --no-edit
git log --oneline -4
cat Notes.txt[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
twogit 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.
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 53fdfe90 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 fileEach 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.
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 reset7d30962 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.
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^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?".
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"?? .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 .gitignoreBefore 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 addmoves changes into the staging area, andgit commitmoves them into the repository. - A branch is a pointer to a commit. Creating and switching branches rewrites no history.
git mergekeeps the history as it happened and adds a merge commit when histories diverged.git rebasereplays 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 --softkeeps changes staged,--mixedkeeps them unstaged, and--harddiscards them.revertadds a new commit that undoes an earlier one and is safe on shared branches.git refloglists previous positions ofHEAD, so a commit lost toreset --hardcan 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 --abortcancels. git bisect run <script>finds the first bad commit with a number of checks that grows with the logarithm of the commit count.git blamenames the last commit that changed each line. It does not name the first..gitignoredoes not affect files that are already tracked.
Summary
git initcreates.git, which holds the whole history.git statusshows untracked, modified, and staged files, and-sprints two status columns.git addstages content, andgit commit -mrecords the staged content with a message.git log --oneline --graph --allshows the shape of the history across branches.git diffcompares the working tree with the staging area, andgit diff --stagedcompares the staging area with the last commit.git branchlists and creates branches, andgit switchandgit switch -cchange 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 mainreplays the branch commits on top ofmainwith new ids.git stash push -msaves uncommitted work, andgit stash poprestores it.git restore filediscards edits, andgit restore --staged fileunstages.git resetmoves the branch with--soft,--mixed, or--hard, andgit revertrecords the inverse of a commit.git reflogfinds commits that no branch reaches, andgit bisectandgit blamelocate the commit behind a bug or a line.
