Beginner Exercises
Create a dedicated practice directory for all exercises: mkdir ~/git-practice && cd ~/git-practice. Each exercise will have its own subdirectory so they don't interfere with each other.
Exercise 1: Your First Repository
Scenario: Start a new project from scratch, make three commits, and explore the history.
- Create a new directory and initialize a Git repo:
mkdir ex1-first-repo && cd ex1-first-repo && git init - Create
README.mdwith "# My First Git Project" and commit it:git add README.md && git commit -m "docs: add README" - Create
index.htmlwith a basic HTML skeleton. Stage and commit:git commit -m "feat: add index.html" - Add a
<h1>tag to index.html. Stage and commit:git commit -m "feat: add page heading" - View the history:
git log --oneline— you should see 3 commits - View the full details of the second commit:
git show HEAD~1
git log --oneline shows 3 commits. git show HEAD~1 shows the diff for the index.html commit.Exercise 2: Branching, Switching, and Merging
Scenario: Add a feature on a branch, then merge it back to main.
- From your repo in ex1, check you're on
main:git branch - Create and switch to a new branch:
git switch -c feature/add-nav - Create
nav.htmlwith navigation links. Commit it on the feature branch. - Add a link to nav.html in index.html. Make another commit on the feature branch.
- Switch back to main:
git switch main - Merge the feature branch:
git merge feature/add-nav - View the graph:
git log --oneline --graph - Delete the feature branch:
git branch -d feature/add-nav
git branch.Exercise 3: Partial Staging with git add -p
Scenario: You've made two unrelated changes in one file. Commit them as separate, focused commits.
- Open
index.htmland make two separate changes: add a<footer>section near the bottom AND fix a typo at the top - Run
git diffto see both changes - Run
git add -p index.htmlto enter interactive staging - Stage only the typo fix (press
yfor the first hunk,nfor the footer hunk) - Commit just the typo:
git commit -m "fix: correct heading typo" - Now stage and commit the footer:
git add index.html && git commit -m "feat: add footer section" - Verify with
git log --oneline— two separate commits
s to split the hunk into smaller pieces.Exercise 4: Discard Changes with git restore
Scenario: You've made changes you don't want to keep.
- Make some experimental changes to
index.html(don't stage them) - Verify with
git diffthat changes are there - Discard all unstaged changes:
git restore index.html - Verify with
git diff— output should be empty now - Now stage a change:
git add index.html - Unstage it:
git restore --staged index.html - Verify it's now unstaged:
git status
git restore (without --staged) discards working directory changes — this cannot be undone! Make sure you really don't want those changes.Exercise 5: Set Up SSH Key for GitHub
Scenario: Configure passwordless authentication with GitHub using SSH.
- Check for existing SSH keys:
ls ~/.ssh/*.pub - If none exist, generate a new key:
ssh-keygen -t ed25519 -C "your-email@example.com" - Start the SSH agent:
eval "$(ssh-agent -s)" - Add your key to the agent:
ssh-add ~/.ssh/id_ed25519 - Copy your public key:
cat ~/.ssh/id_ed25519.pub - On GitHub: Settings → SSH and GPG keys → New SSH key → paste the key
- Test the connection:
ssh -T git@github.com— should say "Hi username! You've successfully authenticated"
Intermediate Exercises
Exercise 6: Create a Branch from a Past Commit
Scenario: A bug was introduced 3 commits ago. Create a branch from the "last good" commit.
- Use
git log --onelineto identify a commit from 3 commits ago and copy its hash - Create a branch starting at that commit:
git switch -c bugfix/from-past <commit-hash> - Verify you're on the right commit:
git log --oneline -3 - Make a fix commit on this branch
- View how your branch diverges:
git log --oneline --graph --all
git switch -c bugfix/from-past HEAD~3 means "3 commits before HEAD".Exercise 7: Simulate and Resolve a Merge Conflict
Scenario: Two branches modify the same line — create and resolve the conflict.
- Create a new repo with one commit (a file with "Hello World" on line 1)
- Create branch A: change "Hello World" to "Hello from Branch A" and commit
- Switch back to main. Create branch B: change "Hello World" to "Hello from Branch B" and commit
- Merge branch A into main:
git merge feature-a - Now merge branch B:
git merge feature-b— this will conflict - Open the conflicted file, see the conflict markers. Choose "Hello from Branch A and B!" as the resolution
- Remove conflict markers, save, then:
git add . && git commit - View the final graph:
git log --oneline --graph --all
Exercise 8: Interactive Rebase – Squash 3 Commits
Scenario: Your feature branch has 3 WIP commits. Squash them into one clean commit before merging.
- Create a new branch and make 3 commits with messages: "WIP", "WIP: more progress", "WIP: done"
- Run interactive rebase on the last 3 commits:
git rebase -i HEAD~3 - In the editor, change the second and third lines from
picktosquash(ors) - Save and close. A new editor opens for the combined commit message — write: "feat: add complete feature"
- Verify with
git log --oneline— 3 commits are now 1
git config --global core.editor "nano" or git config --global core.editor "code --wait" for VS Code.Exercise 9: Cherry-Pick a Commit from Another Branch
Scenario: A useful commit was made on a feature branch — bring it to main without merging the whole branch.
- Create a branch
feature/experimentsand make 3 commits (add features A, B, and C) - Switch back to main:
git switch main - View the feature branch commits:
git log --oneline feature/experiments - Copy the hash of the "feature B" commit (the middle one)
- Cherry-pick just that commit:
git cherry-pick <hash> - Verify main has "feature B" but not A or C:
git log --oneline
Exercise 10: Create an Annotated Tag
Scenario: Mark your project's first release with an annotated tag and push it.
- Make sure you have at least 3 commits on main
- Create an annotated tag:
git tag -a v1.0.0 -m "Release v1.0.0 – first stable release" - View tag details:
git show v1.0.0 - List all tags:
git tag - If you have a GitHub repo: push the tag:
git push origin v1.0.0 - Verify the tag appears on GitHub under Releases → Tags
Advanced Exercises
Exercise 11: Use git bisect to Find a Bug
Scenario: A project has 10 commits, and one of them introduced a bug. Find it with bisect.
- Create a repo and make 10 commits. In commit 6 (not the last one), introduce a "bug": add a line
const BUG = true;to a file - Start bisect:
git bisect start - Mark current HEAD as bad:
git bisect bad - Mark the first commit as good:
git bisect good <first-commit-hash> - For each checkout, check if the file contains "BUG = true". Mark good or bad accordingly.
- Continue until Git identifies the first bad commit
- Verify it found commit 6
- Reset:
git bisect reset
grep -r "BUG = true" . at each checkout to test for the bug. With 10 commits, bisect will find it in at most 4 steps.Exercise 12: Write a pre-commit Hook That Checks for console.log
Scenario: Prevent console.log statements from being committed to the repository.
- Create a new repo with a
src/directory and a JavaScript file - Create the pre-commit hook:
touch .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit - Write the hook script to check staged JS files for
console.log:
#!/bin/sh
STAGED=$(git diff --cached --name-only --diff-filter=ACM | grep "\.js$")
if [ -n "$STAGED" ]; then
if git diff --cached "$STAGED" | grep "^+.*console\.log"; then
echo "Error: Remove console.log before committing"
exit 1
fi
fi
exit 0
- Add
console.log("debug")to a JS file and try to commit — it should be rejected - Remove the console.log and commit again — it should succeed
Exercise 13: Set Up Conventional Commits with commitlint
Scenario: Enforce Conventional Commits format on every commit in a Node.js project.
- Create a Node.js project:
npm init -y - Install commitlint and Husky:
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky - Create commitlint config:
echo "module.exports = {extends:['@commitlint/config-conventional']}" > commitlint.config.js - Initialize Husky:
npx husky init - Add commit-msg hook:
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg - Test with a bad commit:
git commit -m "changed stuff"— should fail - Test with a good commit:
git commit -m "feat: add initial setup"— should pass
Exercise 14: Fork a Repo, Sync Upstream, Open a PR
Scenario: Practice the open-source contribution workflow with a real GitHub fork.
- On GitHub, fork any public repository (e.g., a documentation repo or a simple tool)
- Clone your fork:
git clone git@github.com:YOUR-USERNAME/repo-name.git - Add the original as upstream:
git remote add upstream git@github.com:ORIGINAL-OWNER/repo-name.git - Verify remotes:
git remote -v— should show both origin and upstream - Create a feature branch:
git switch -c fix/typo-in-readme - Make a small change (fix a typo, improve a sentence)
- Push to your fork:
git push -u origin fix/typo-in-readme - Open a PR from your fork to the original repo on GitHub
Exercise 15: Simulate a Git Flow Release Cycle
Scenario: Practice the full Git Flow branching model for one complete release.
- Create a repo with
mainanddevelopbranches:git switch -c develop - Create and complete two feature branches:
feature/loginandfeature/dashboard— both merge back to develop - Create a release branch:
git switch -c release/1.0.0 develop - Make a minor fix on the release branch (e.g., update version number in package.json)
- Merge release to main:
git switch main && git merge --no-ff release/1.0.0 - Tag the release:
git tag -a v1.0.0 -m "Release v1.0.0" - Merge release back to develop:
git switch develop && git merge --no-ff release/1.0.0 - Delete the release branch
- View the full history:
git log --oneline --graph --all
📋 Summary
- Beginner: Init repo → commit → branch → merge → partial staging → discard changes → SSH setup.
- Intermediate: Branch from past commit → resolve conflicts → interactive rebase/squash → cherry-pick → annotated tags.
- Advanced: git bisect → pre-commit hooks → commitlint + Husky → fork/PR workflow → full Git Flow cycle.
- The best way to learn Git is by doing — run every exercise in a real terminal.
- Make mistakes on purpose: try to break things, then figure out how to fix them using reflog.
FAQ
The easiest fix: delete the exercise directory and start fresh — cd .. && rm -rf ex1-first-repo && mkdir ex1-first-repo && cd ex1-first-repo && git init. For practice repos, this is completely fine. For recovery without starting over: git reflog shows every recent HEAD position — find the state you want to return to and run git reset --hard <reflog-hash>. If things are really confused, git status usually gives you hints about what state you're in and what command to run.
For most exercises (1–12), no — everything is local. GitHub is only needed for Exercise 5 (SSH key setup), Exercise 10 (pushing a tag), Exercise 14 (fork and PR workflow), and optionally Exercise 15. If you don't have a GitHub account, you can simulate the remote using a second local clone: git clone --bare /path/to/repo /path/to/bare-remote.git and use that as your "remote" for push/pull exercises.
Create one quickly with a shell script: for i in $(seq 1 10); do echo "change $i" >> file.txt; git add .; git commit -m "commit $i"; done. Then use a text editor to add the "bug" marker to the file at a specific commit using interactive rebase (git rebase -i HEAD~5 and use the edit command to stop at commit 6, add the bug text, git add . && git commit --amend --no-edit, then git rebase --continue).