Hands-on Git Exercises: Practical Tasks and Challenges
4 hours total
Practice · Exercises
Updated: Feb 2026
Learn Git by Doing: A Hands-on Approach
The best way to master Git is through deliberate practice. This guide contains 15 hands-on exercises ranging from basic commands to advanced recovery scenarios. Each exercise includes a clear objective, step-by-step instructions, hints, and a detailed solution. Create a practice repository and work through these exercises to build real Git fluency.
Getting Started: Create a practice directory and initialize a Git repository. All exercises can be done locally—no remote connection needed. Use mkdir git-practice && cd git-practice && git init to begin.
📊 Your Progress Tracker
0/15 exercises completed
🟢 Beginner🟡 Intermediate🔴 Advanced🔵 Challenge
📝 Exercise 1: First Commit
🌿 Exercise 2: Branch Basics
🔀 Exercise 3: Merge Practice
⚡ Exercise 4: Rebase Workflow
💥 Exercise 5: Conflict Resolution
⏪ Exercise 6: Undo Changes
🆘 Exercise 7: Reflog Recovery
🔍 Exercise 8: Bisect Debug
📦 Exercise 9: Stash Management
🏷️ Exercise 10: Tagging Releases
🔄 Exercise 11: Interactive Rebase
🔐 Exercise 12: Signed Commits
🧹 Exercise 13: Clean History
🚑 Exercise 14: Hotfix Simulation
🎯 Exercise 15: Recovery Challenge
Exercise 1
Your First Commit
Beginner
🎯 Objective: Create your first Git repository, add files, and make your initial commit. Understand the basic Git workflow.
📝 Tasks:
Create a new directory called git-exercise-1 and initialize a Git repository
Create a file called README.md with the content "# My Git Practice"
Create a second file called hello.txt with the content "Hello, Git!"
Check the status of your repository
Stage both files for commit
Commit the files with the message "Initial commit: add README and hello.txt"
View the commit history
💡 Hint: Use git status frequently to see what's happening. Remember that git add stages files, and git commit -m "message" creates the commit.
✅ Solution:
$ mkdir git-exercise-1 $ cd git-exercise-1 $ git init Initialized empty Git repository in /path/git-exercise-1/.git/
Explanation: This exercise demonstrates the fundamental Git workflow: working directory → staging area (git add) → repository (git commit). The commit hash (a1b2c3d) will be different for you.
Exercise 2
Branching Basics
Beginner
🎯 Objective: Learn to create, switch between, and delete branches. Understand that branches are lightweight pointers.
📝 Tasks:
From your repository in Exercise 1, create a new branch called feature/update-files
Switch to the new branch
Append "This is a new line" to hello.txt
Create a new file notes.txt with any content
Commit these changes with message "Update files on feature branch"
Switch back to the main branch and verify that the changes are not visible
List all branches and identify which one is active
Delete the feature branch (after noting you can't delete the current branch)
💡 Hint: Use git branch to list branches, git checkout -b to create and switch, or git switch -c for the newer syntax. The -d flag deletes a branch.
✅ Solution:
$ git branch feature/update-files $ git checkout feature/update-files Switched to branch 'feature/update-files' # Or in one command: git checkout -b feature/update-files
$ echo "This is a new line" >> hello.txt $ echo "Some notes" > notes.txt $ git add hello.txt notes.txt $ git commit -m "Update files on feature branch" [feature/update-files b2c3d4e] Update files on feature branch
$ git checkout main Switched to branch 'main' $ cat hello.txt Hello, Git! # No new line - changes isolated to branch
Explanation: Branches in Git are just pointers to commits. When you switch branches, Git updates your working directory to match the branch's commit. This isolation is why branches are so powerful for parallel development.
Exercise 3
Merge Practice
Beginner
🎯 Objective: Practice merging branches and understand fast-forward vs. non-fast-forward merges.
📝 Tasks:
Create a new branch feature/add-content from main
On the feature branch, create a file content.txt with three lines of text
Commit this change
Switch back to main and create a different file main-content.txt with some text
Commit this change on main
Now merge the feature branch into main
Observe that this creates a merge commit (non-fast-forward)
View the commit graph with git log --graph
💡 Hint: Because both branches have new commits, Git cannot fast-forward. It will create a merge commit. Use git log --graph --oneline to see the branch structure.
Explanation: The merge commit (e5f6g7h) has two parents: c3d4e5f (main's commit) and d4e5f6g (feature branch's commit). This preserves the history showing that parallel development occurred.
Exercise 4
Rebase Workflow
Intermediate
🎯 Objective: Understand rebasing by replaying commits on top of another branch to create linear history.
📝 Tasks:
Create a new branch feature/rebase-practice from main
On this branch, create two commits: add file feature1.txt and then feature2.txt
Switch to main and add a commit (create main-update.txt)
Instead of merging, rebase the feature branch onto main
Observe how the commit history becomes linear
Compare the commit hashes before and after rebase
💡 Hint: While on the feature branch, run git rebase main. Git will temporarily remove your feature commits, apply main's new commits, then reapply your feature commits on top.
Explanation: After rebase, the feature branch commits (e6f7g8h, f7g8h9i) have new hashes—they're new commits applied after main's d5e6f7g. History is linear, as if feature work started after main's update.
Key Insight: The original feature commits still exist but are now unreachable (until garbage collected).
Exercise 5
Conflict Resolution
Intermediate
🎯 Objective: Create and resolve a merge conflict. Learn to identify conflict markers and choose the correct resolution.
📝 Tasks:
Create a file conflict.txt on main with content "Main line 1\nMain line 2" and commit it
Create and switch to a branch feature/conflict
On the feature branch, modify conflict.txt to change line 2 to "Feature line 2" and commit
Switch back to main and modify the SAME line 2 to "Main updated line 2" and commit
Attempt to merge the feature branch into main—observe the conflict
Resolve the conflict by keeping both versions or choosing one
Complete the merge and verify the result
💡 Hint: After merge conflict, open conflict.txt and look for <<<<<<<, =======, and >>>>>>> markers. Edit to resolve, then git add and git commit.
✅ Solution:
# Setup on main $ echo -e "Main line 1\nMain line 2" > conflict.txt $ git add conflict.txt && git commit -m "Add conflict.txt"
# Feature branch changes $ git checkout -b feature/conflict $ sed -i '' '2s/.*/Feature line 2/' conflict.txt # Replace line 2 $ git add conflict.txt && git commit -m "Modify line 2 on feature"
# Main branch changes $ git checkout main $ sed -i '' '2s/.*/Main updated line 2/' conflict.txt $ git add conflict.txt && git commit -m "Modify line 2 on main"
# View conflict markers $ cat conflict.txt Main line 1
<<<<<<< HEAD
Main updated line 2
=======
Feature line 2
>>>>>>> feature/conflict
# Resolve by keeping both $ echo -e "Main line 1\nMain updated line 2 (main)\nFeature line 2 (feature)" > conflict.txt $ git add conflict.txt $ git commit -m "Merge feature/conflict and resolve conflict" [main g8h9i0j] Merge feature/conflict and resolve conflict
Explanation: Conflict markers show both versions. HEAD is your current branch (main), the other is the incoming branch. After resolving, staging and committing completes the merge.
Exercise 6
Undoing Changes
Intermediate
🎯 Objective: Practice various undo operations: unstage files, amend commits, revert commits, and reset commits.
📝 Tasks:
Create a file mistake.txt and stage it with git add
Unstage the file without discarding changes
Create a commit with a typo in the message (e.g., "Inital commit")
Amend the commit to fix the message
Create three commits: A, B, C in sequence
Use git revert to undo commit B (the middle one)
Use git reset to go back to commit A (discard C and the revert)
Use git reflog to find and return to your original state
💡 Hint: Remember: git reset has three modes: --soft (keep changes staged), --mixed (keep changes unstaged), --hard (discard changes). Use reflog as your safety net.
✅ Solution:
# Part 1: Unstage $ echo "mistake" > mistake.txt $ git add mistake.txt $ git status # Shows staged $ git restore --staged mistake.txt # Unstage $ git status # Now untracked
# Revert commit B (find its hash) $ git log --oneline c3d4e5f Commit C
b2c3d4e Commit B
a1b2c3d Commit A $ git revert b2c3d4e [main d4e5f6g] Revert "Commit B"
# Reset to Commit A (discard C and revert) $ git reset --hard a1b2c3d HEAD is now at a1b2c3d Commit A
# Find lost commits in reflog $ git reflog a1b2c3d HEAD@{0}: reset: moving to a1b2c3d
d4e5f6g HEAD@{1}: revert: Revert "Commit B"
c3d4e5f HEAD@{2}: commit: Commit C $ git reset --hard c3d4e5f # Return to Commit C HEAD is now at c3d4e5f Commit C
Explanation: Revert creates a new commit that undoes changes (safe for shared branches). Reset moves branch pointers (destructive for history). Reflog tracks all movements, allowing recovery.
Exercise 7
Reflog Recovery
Intermediate
🎯 Objective: Learn to use reflog to recover "lost" commits after destructive operations.
📝 Tasks:
Create three commits: Commit1, Commit2, Commit3
Use git reset --hard HEAD~2 to "lose" the last two commits
Verify the commits are gone from git log
Use git reflog to find the lost commits
Recover Commit3 by resetting to its hash
Create a branch at Commit2 to preserve it
💡 Hint: Reflog shows every movement of HEAD. Look for the "commit:" entries before your reset. The hashes are still valid even though they're not in the log.
# Lose commits $ git reset --hard HEAD~2 HEAD is now at a1b2c3d Commit1 $ git log --oneline # Shows only Commit1
# Find lost commits in reflog $ git reflog a1b2c3d HEAD@{0}: reset: moving to HEAD~2
d4e5f6g HEAD@{1}: commit: Commit3
b2c3d4e HEAD@{2}: commit: Commit2
a1b2c3d HEAD@{3}: commit: Commit1
# Recover Commit3 $ git reset --hard d4e5f6g HEAD is now at d4e5f6g Commit3 $ git log --oneline # Commit3 is back!
# Create branch at Commit2 $ git branch recovered-commit2 b2c3d4e $ git branch # Shows new branch
Explanation: Reflog is your safety net. It records every action that moves HEAD for 90 days by default. Even after reset --hard, commits remain in the object database and can be recovered via reflog.
Exercise 8
Git Bisect Debugging
Advanced
🎯 Objective: Use git bisect to perform a binary search and find which commit introduced a "bug".
📝 Tasks:
Create 10 commits, each adding a line to numbers.txt with a number from 1 to 10
In commit 5, introduce a "bug" by writing "BUG" instead of "5"
Now, pretending you don't know where the bug is, use git bisect to find it
Mark the last commit as bad (contains bug) and first commit as good (clean)
At each bisect step, check if the file contains "BUG"
Continue until bisect identifies the exact commit
💡 Hint: Use git bisect start, then git bisect bad and git bisect good. Git will checkout middle commits. Test each, mark good/bad until the culprit is found.
✅ Solution:
# Create commits with "bug" at commit 5 $ for i in {1..10}; do
if [ $i -eq 5 ]; then echo "BUG" >> numbers.txt;
else echo $i >> numbers.txt; fi
git add numbers.txt && git commit -m "Add number $i"
done
# Start bisect $ git bisect start $ git bisect bad HEAD # Last commit is bad $ git bisect good HEAD~9 # First commit (10 commits ago) is good Bisecting: 4 revisions left to test
# Git checks out middle commit - test it $ cat numbers.txt | tail -1 # See last line 4 $ git bisect good # No BUG yet
# Git checks another commit $ cat numbers.txt | tail -1 6 $ git bisect bad # Contains BUG? Check full file $ grep BUG numbers.txt # Found! This commit has the bug
# Continue until bisect identifies the exact commit c3d4e5f is the first bad commit
# End bisect $ git bisect reset
Explanation: Bisect performs a binary search through commit history. At each step, you mark the current commit as good (no bug) or bad (bug present). Git narrows down to the exact commit that introduced the problem.
Exercise 9
Stash Management
Intermediate
🎯 Objective: Master git stash for temporarily saving work-in-progress changes.
📝 Tasks:
On your main branch, create a file work-in-progress.txt with some content but DO NOT commit it
You need to switch branches urgently, so stash your changes
Verify the working directory is clean
Create and switch to a new branch urgent-fix, make a commit, and switch back
Apply your stash back
Create another stash with a custom message
List all stashes and apply a specific one
Create a branch from a stash
💡 Hint: Use git stash push -m "message" for named stashes. git stash list shows all. git stash apply stash@{1} applies a specific stash. git stash branch creates a branch from a stash.
✅ Solution:
# Create uncommitted work $ echo "Important work in progress" > work-in-progress.txt $ git status Untracked: work-in-progress.txt
# Stash changes $ git stash push -m "WIP on main feature" Saved working directory and index state On main: WIP on main feature $ git status # Clean!
# Work on urgent fix $ git checkout -b urgent-fix $ echo "fix" > fix.txt && git add fix.txt && git commit -m "Urgent fix" $ git checkout main
# Apply stash back $ git stash apply On main: Changes to be committed: new file: work-in-progress.txt
# List stashes $ git stash list stash@{0}: On main: Additional work
stash@{1}: On main: WIP on main feature
# Apply specific stash $ git stash apply stash@{1}
# Create branch from stash $ git stash branch new-feature stash@{0} Switched to branch 'new-feature'
Explanation: Stashes are like temporary commits that aren't on any branch. They're useful for context switching. The stash branch command creates a branch starting at the commit where the stash was created and applies the stash.
Exercise 10
Tagging Releases
Intermediate
🎯 Objective: Practice creating and managing both lightweight and annotated tags for releases.
📝 Tasks:
Create three commits representing versions: v1.0, v1.1, and v1.2
Create a lightweight tag called v1.0 on the first commit
Create an annotated tag v1.1 with message "Release version 1.1"
Create a signed tag v1.2 (if GPG configured)
List all tags
Show details of the annotated tag
Checkout a tag (observe detached HEAD)
Push tags to a remote (simulate by adding a local remote)
Delete a tag locally
💡 Hint: Use git tag v1.0 (lightweight), git tag -a v1.1 -m "message" (annotated), git tag -s v1.2 -m "message" (signed). git show v1.1 shows tag details.
# Delete tag locally $ git tag -d v1.0 Deleted tag 'v1.0'
Explanation: Lightweight tags are just pointers (like branches that don't move). Annotated tags store additional metadata: tagger, date, message. Signed tags add cryptographic verification. Tags are not automatically pushed—use --tags or specific tag names.
Exercise 11
Interactive Rebase
Advanced
🎯 Objective: Master interactive rebase to squash, reword, edit, and reorder commits.
# Save and close. Git will: # 1. Apply WIP 1 # 2. Prompt to reword "Add feature" → type "feat: implement login feature" # 3. Apply "Fix typo" and squash it into previous commit # 4. Apply "Add tests" (now before the feature commits in timeline) # 5. Drop "WIP 2"
Explanation: Interactive rebase rewrites history by creating new commits. The original commits still exist but become unreachable. This is powerful for cleaning up before sharing, but dangerous on already-pushed branches.
Exercise 12
Signed Commits
Advanced
🎯 Objective: Configure and create signed commits using GPG to verify authorship.
📝 Tasks:
Generate a GPG key pair (if you don't have one)
Configure Git to use your GPG key for signing
Create a signed commit
View the commit signature in the log
Create a signed tag
Configure Git to sign all commits automatically
💡 Hint: Use gpg --full-generate-key to create a key. Find your key ID with gpg --list-secret-keys. Configure with git config --global user.signingkey [key-id]. Sign commits with -S flag.
✅ Solution:
# Generate GPG key (interactive) $ gpg --full-generate-key Please select what kind of key you want: (1) RSA and RSA
What keysize? 4096
Key is valid for? 1y
# List your keys $ gpg --list-secret-keys --keyid-format LONG sec rsa4096/A1B2C3D4E5F67890 2024-01-01 [SC]
# Configure Git to use your key $ git config --global user.signingkey A1B2C3D4E5F67890
# Create a signed commit $ echo "signed content" > signed.txt $ git add signed.txt $ git commit -S -m "This is a signed commit" [main a1b2c3d] This is a signed commit
1 file changed, 1 insertion(+)
create mode 100644 signed.txt
# Verify signature $ git log --show-signature -1 commit a1b2c3d...
gpg: Signature made ...
gpg: Good signature from "Your Name "
# Create signed tag $ git tag -s v1.0 -m "Signed release v1.0"
# Configure auto-signing for all commits $ git config --global commit.gpgSign true $ git config --global tag.gpgSign true
Explanation: Signed commits provide cryptographic proof that the commit came from you and hasn't been tampered with. The signature is stored in the commit object. GitHub/GitLab show "Verified" badges for signed commits when the public key is uploaded.
Exercise 13
Cleaning History
Advanced
🎯 Objective: Use git filter-repo to remove sensitive files or large files from repository history.
📝 Tasks:
Create a repository with several commits, including one that accidentally added a file secret.txt containing "password=12345"
Add more commits after the secret was added
Use git filter-repo to completely remove secret.txt from all history
Verify the file is gone from all commits
Note that commit hashes have changed
💡 Hint: Install git-filter-repo with pip install git-filter-repo. Then run git filter-repo --path secret.txt --invert-paths to remove the file from all commits.
# Install filter-repo if needed $ pip install git-filter-repo
# Remove secret.txt from all history $ git filter-repo --path secret.txt --invert-paths Parsed 3 commits
New history written in 2.3 seconds
Ref 'refs/heads/main' was rewritten
# Verify secret is gone $ git log --all --oneline d4e5f6g Update normal file
e5f6g7h Add normal file # Secret commit completely removed $ ls # secret.txt no longer exists in any commit normal.txt
Explanation: filter-repo rewrites history by creating new commits that omit the specified paths. Original commits are discarded. This is necessary for removing secrets, but forces everyone to re-clone. Always rotate secrets BEFORE cleaning history.
⚠️ Warning: After filter-repo, all commit hashes change. Team members must re-clone or carefully reset.
Exercise 14
Hotfix Simulation
Advanced
🎯 Objective: Simulate a production hotfix scenario: create a hotfix branch, apply fix, deploy, and ensure fix is merged to all branches.
📝 Tasks:
Create a repository with a main branch (production) and a develop branch
Add a release tag v1.0.0 on main
On develop, add several new features (simulate with commits)
Simulate a critical bug found in production (in v1.0.0)
Create a hotfix branch from the v1.0.0 tag
Apply the fix and commit with message "hotfix: critical security patch"
Merge hotfix to main and tag as v1.0.1
Also merge hotfix to develop
Verify that develop still has its features AND the hotfix
💡 Hint: The critical step is merging the hotfix to develop. If you skip this, the bug will reappear in the next release from develop.
✅ Solution:
# Setup: main and develop branches $ git init hotfix-sim $ cd hotfix-sim $ echo "v1.0 code" > app.txt $ git add app.txt && git commit -m "Release v1.0" $ git tag -a v1.0.0 -m "Production release v1.0.0"
# Hotfix: create branch from production tag $ git checkout -b hotfix/security v1.0.0 $ echo "security patch" >> app.txt # Fix the bug $ git add app.txt && git commit -m "hotfix: critical security patch"
# Merge to main and tag $ git checkout main $ git merge --no-ff hotfix/security -m "Merge hotfix v1.0.1" $ git tag -a v1.0.1 -m "Hotfix release v1.0.1"
# CRITICAL: Also merge to develop $ git checkout develop $ git merge --no-ff hotfix/security -m "Merge hotfix into develop"
# Verify both features and fix exist on develop $ git log --oneline f6g7h8i Merge hotfix into develop
g7h8i9j Add feature 2
h8i9j0k Add feature 1
i9j0k1l hotfix: critical security patch
...
# Clean up $ git branch -d hotfix/security
Explanation: The hotfix branch starts from the production tag, not from main, ensuring it's based exactly on what's deployed. Merging to both main and develop ensures the fix persists. If you skip develop, the bug returns in the next release.
Exercise 15
Ultimate Recovery Challenge
Challenge
🎯 Objective: Combine multiple recovery techniques to rescue a seemingly lost repository state.
📝 Challenge Description:
You've been given a repository that has suffered multiple disasters:
A developer ran git reset --hard HEAD~5 and lost 5 commits
Another developer deleted a branch that contained important work
Someone accidentally ran git gc and pruned some objects
There's a dangling commit that contains a file with the word "RECOVER-ME"
Your task: Find and recover all lost work. Document each step and the final state.
💡 Hint: You'll need: reflog to find lost commits, fsck to find dangling objects, git log -g to search reflog messages, and git branch to restore branches. Work systematically.
✅ Solution Approach:
# Step 1: Check reflog for recent history $ git reflog | head -20 a1b2c3d HEAD@{0}: reset: moving to HEAD~5
e4f5g6h HEAD@{1}: commit: Add important feature
i7j8k9l HEAD@{2}: commit: Update API
m0n1o2p HEAD@{3}: commit: Fix bug
...
# Step 2: Recover lost commits from reflog $ git reset --hard e4f5g6h # Restore to before reset
# Step 3: Check for deleted branches $ git reflog --all | grep branch a1b2c3d refs/heads/deleted-branch@{1}: commit: Work on feature $ git branch recovered-branch a1b2c3d
# Step 7: Check dangling blob content $ git show g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4 This file contains: RECOVER-ME-IMPORTANT-DATA $ git show g6h7i8... > recovered-file.txt
# Step 8: Verify all recovered $ git log --oneline --all --graph $ ls -la recovered-file.txt
Explanation: This challenge combines all recovery techniques:
Reflog recovers recent lost commits and deleted branches
fsck --lost-found finds dangling commits and blobs that aren't referenced
Each recovered object must be examined and restored via branches or file extraction
Always work from most recent to oldest, and document what you find
💡 Pro Tip: In real disaster recovery, first make a full backup of the .git directory before attempting any recovery.
🎉 Congratulations! You've completed all 15 hands-on exercises. You've practiced basic workflows, branching strategies, conflict resolution, recovery techniques, and advanced operations. These skills translate directly to real-world Git usage.
Next Steps: Create your own practice scenarios. Intentionally break repositories and practice recovery. The more you practice, the more confident you'll become.
After completing Exercise 14 (Hotfix Simulation), why is it critical to merge the hotfix into the develop branch as well as main?
To ensure the bug fix is included in future releases from develop
Because Git requires merging to all branches
To create more commits for the release notes
To test the fix in both branches
Exercise FAQ
How should I practice these exercises effectively?
Effective practice strategy:
Create a dedicated practice directory: Keep practice repos separate from real work.
Start with a clean slate: Run rm -rf .git and git init to reset between exercises if needed.
Don't peek at solutions immediately: Try each exercise for at least 10-15 minutes before checking the solution.
Experiment beyond the tasks: After completing an exercise, try variations—what happens if you do X instead of Y?
Use git status frequently: After every command, see what changed.
Document your learning: Keep a journal of commands you learned and scenarios you practiced.
Repeat exercises after a week: Spaced repetition builds long-term memory.
What if I make a mistake during an exercise?
Making mistakes is part of learning! Here's how to handle them:
Use git reflog: Most mistakes can be undone by finding the pre-mistake state in reflog and resetting.
Start over: For practice repos, it's often fastest to delete the .git folder and re-initialize.
Learn from the mistake: Understand what went wrong—it's often more valuable than getting it right the first time.
Use git status and git log: These commands help you understand your current state.
When truly stuck: Check the solution, but then try to reproduce the exercise from memory.
How do these exercises translate to real-world scenarios?
Each exercise maps to common real-world situations:
Exercises 1-3: Daily development work—creating commits, branches, merges
Exercise 4-5: Keeping feature branches updated, handling team conflicts
Exercise 6: Fixing mistakes—every developer needs this
Exercise 7-8: Debugging and recovery—essential for senior roles
Exercise 9: Context switching—happens multiple times daily
Exercise 10: Release management—crucial for DevOps
Exercise 11: History cleanup before PRs—professional workflow