PART 01
What is Version Control?
Version control is a system that records changes to files over time, so you can recall specific versions later. Think of it like save points in a video game — you can always go back to a previous state.
Without version control, teams resort to emailing files, naming them project_v2_final_FINAL2.zip, and hoping nobody overwrites each other's work.
Why Version Control Matters
- Collaboration — Multiple developers can work on the same project without overwriting each other
- History — Every change is logged with who made it, when, and why
- Backup — Your code is never truly lost
- Experimentation — Try new features in isolation without breaking the main project
- Accountability — Track bugs back to the exact commit that introduced them
- Code Review — Teams can review and discuss changes before they are merged
Types of Version Control
| Type | Description | Examples |
| Local | Tracks changes only on your own machine | RCS |
| Centralized | Single server holds all history; clients check out files | SVN, CVS, Perforce |
| Distributed | Every client has the full repository including history | Git, Mercurial |
💡
Why Git Wins
Git is distributed — every developer has a complete copy of the entire history. You can work offline, commit locally, and sync later. Most operations are local, making it extremely fast.
PART 02
How Git Works Internally
Before memorizing commands, understand the mental model. This is what separates developers who use Git confidently from those who fear it.
The Three Trees of Git
Git manages your project through three distinct environments called "trees". Every Git command moves changes between these trees.
📁
Working Directory
your live files
🏛️
Repository
.git folder
1. Working Directory
This is the folder on your computer where your project lives — the files you see and edit in VS Code or any editor. Files here can be tracked (Git knows about them) or untracked (Git has never seen them).
- Any edit you make — adding a line, deleting a file — happens here first
- Git does NOT automatically save these changes
- Run
git status to see what is changed in your working directory
2. Staging Area (The Index)
The staging area is the most misunderstood part of Git. It is a middle layer — a preparation zone where you carefully select exactly which changes go into your next commit. Think of it like packing a box before shipping — you choose specifically what goes in.
# You fixed a bug AND added a feature in the same work session.
# Without staging: forced to commit both with one messy message.
# With staging:
git add bugfix.py
git commit -m "Fix: null pointer error in login"
git add new_feature.py
git commit -m "Feat: add dark mode toggle"
# Two clean, separate, meaningful commits from the same work session!
3. Repository (.git Folder)
This is Git's permanent database — hidden inside a .git folder in your project root. When you run git commit, Git takes a permanent snapshot of everything in the staging area and saves it here forever.
.git/
├── objects/ ← all your file snapshots (blobs, trees, commits)
├── refs/ ← branch and tag pointers
├── HEAD ← which branch you are currently on
├── config ← repo-level settings
└── index ← the staging area file
Snapshots, Not Diffs
Most VCS store a base file plus a list of changes (deltas). Git works differently — it stores full snapshots of every file at each commit. If a file hasn't changed, Git just stores a pointer to the previous identical version.
Every piece of data Git stores is identified by a SHA-1 hash — a 40-character fingerprint like a3f4c9d2b1e8... computed from the content itself.
Commit Object
│ ├── message: "Add Customer model"
│ ├── author: Jobin Jose
│ ├── timestamp: 2026-03-06
│ └── pointer to: Tree Object + Parent Commit
│
Tree Object (represents a directory)
│ ├── Blob → README.md
│ ├── Blob → models.py
│ └── Tree → app/
│ └── Blob → app/views.py
│
Blob Object (represents file content — no filename stored here)
│
Tag Object (named pointer to a specific commit)
The HEAD Pointer
HEAD is a pointer that says "you are here." It tells Git which branch (and thus which commit) you are currently working on. When you switch branches, Git moves HEAD and updates your working directory.
main: A──B──C──D ← HEAD → main → D
main: A──B──C──D
feature: └──E──F ← HEAD → feature/login → F
Understanding git status & git diff
$ git status
On branch main
Changes to be committed: ← STAGING AREA differs from last commit
(use "git restore --staged <file>" to unstage)
modified: views.py ← staged and ready ✓
Changes not staged for commit: ← WORKING DIR differs from staging area
(use "git add <file>" to update what will be committed)
modified: models.py ← edited but not staged
Untracked files: ← WORKING DIR only, never been tracked
serializers.py
@@-12,6 +12,8 @@ class Customer(models.Model):
id = models.AutoField(primary_key=True)
- name = CharField()
+ name = CharField(max_length=200)
+ email = EmailField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
git diff # Working Dir vs Staging Area (unstaged changes)
git diff --staged # Staging Area vs Last Commit (what will be committed)
git diff HEAD # Working Dir vs Last Commit (all changes combined)
git diff main..feature/login # Compare two branches
PART 03
Getting Started — Beginner
● Beginner Level
Installation & Configuration
# macOS
brew install git
# Ubuntu / Debian Linux
sudo apt update && sudo apt install git
# Windows — download installer from https://git-scm.com
# Verify
git --version
git version 2.43.0
git config --global user.name
"Your Name"
git config --global user.email
"[email protected]"
git config --global core.editor
"code --wait" # VS Code
git config --global init.defaultBranch
main
# View all settings
git config --list
Your First Repository & Commit
# Step 1 — Create a project folder and initialize Git
mkdir my-project && cd my-project
git init
# Initialized empty Git repository in /my-project/.git/
# Step 2 — Create a file
echo "# My Project" > README.md
# Step 3 — Check status (always do this before staging)
git status
# Untracked files: README.md
# Step 4 — Stage the file
git add README.md
# Or stage ALL changes: git add .
# Step 5 — Commit with a clear message
git commit -m "Initial commit: add README"
# Step 6 — View your history
git log --oneline
Writing Good Commit Messages
A commit message should complete the sentence: "If applied, this commit will..."
| ❌ Bad Message | ✅ Good Message |
| fix stuff | Fix: null pointer crash when user email is empty |
| update | Refactor: extract payment logic into PaymentService |
| asdf | Feat: add GST invoice PDF export with e-way bill |
| wip | WIP: incomplete — do not merge (inventory module) |
Connecting to GitHub
# First create a NEW empty repo on github.com (no README)
# Then run these commands:
git remote add origin https://github.com/yourname/my-project.git
git branch -M main
git push -u origin main
# -u sets 'origin main' as default upstream for future pushes
# After this, just run:
git push
git pull
git clone https://github.com/yourname/my-project.git
# Clone into a specific folder name
git clone https://github.com/yourname/my-project.git myapp
# This automatically sets up the remote named 'origin'
PART 04
Branching & Collaboration
● Intermediate Level
A branch is a lightweight movable pointer to a commit. Creating a branch is nearly instant — Git creates a new pointer, not a copy of your files.
git branch # list all local branches
git switch -c feature/login # create AND switch (modern syntax)
git switch main # switch back to main
git branch -a # see all branches including remote
git branch -d feature/login # delete merged branch
# 1. Start from a clean main
git switch main && git pull
# 2. Create your feature branch
git switch -c feature/gst-invoice
# 3. Work and commit regularly
git add . && git commit -m "Feat: add GST invoice model"
git add . && git commit -m "Feat: add PDF export endpoint"
# 4. Push branch to GitHub
git push -u origin feature/gst-invoice
# 5. Merge back when done
git switch main
git merge feature/gst-invoice
git branch -d feature/gst-invoice
Merging
main: A──B
feature: C──D
A──B──C──D
main: A──B──E──F
feature: └──C──D
A──B──E──F──M
└──C──D──┘
Resolving Merge Conflicts
A conflict happens when two branches edit the same lines of the same file. Git marks the conflict and asks you to resolve it manually.
<<<<<<< HEAD
login_field = "email" # your current branch
=======
login_field = "phone" # branch being merged in
>>>>>>> feature/phone-login
# 1. Git tells you which files conflict
git status
# both modified: auth/views.py
# 2. Open the file, choose the correct code, remove ALL markers
login_field = "email_or_phone" # your final decision
# 3. Stage the resolved file
git add auth/views.py
# 4. Complete the merge
git commit -m "Merge: resolve login field conflict"
# To abort entirely and go back to before the merge:
git merge --abort
Stashing
git stash # stash all tracked changes
git stash push -u -m "WIP: login" # stash + include untracked files
git stash list # see all stashes
git stash pop # restore latest stash and delete it
git stash apply stash@{1} # restore specific stash (keep it)
git stash drop stash@{0} # delete a stash
git stash clear # delete ALL stashes
.gitignore
# Python / Django
__pycache__/
*.pyc
.env
*.sqlite3
.venv/
staticfiles/
# Flutter / Dart
build/
.dart_tool/
*.g.dart
# Editor
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
# Secrets — NEVER commit these!
*.pem
*.key
secrets.json
🚨
Critical Security Rule
Never commit .env files, API keys, database passwords, or private keys. Once pushed to a public GitHub repo, credentials are permanently exposed — even if you delete them later. Use environment variables and secrets managers.
Pull Requests (GitHub)
- Create a feature branch:
git switch -c fix/navbar-bug
- Make changes, commit, and push:
git push origin fix/navbar-bug
- On GitHub → click "Compare & Pull Request"
- Write a clear description of what changed and why
- Request reviewers and address their feedback comments
- After approval → click "Merge Pull Request"
Undoing Changes
| Situation | Command | Safe for Shared? |
| Discard working dir changes | git restore <file> | ✓ Yes |
| Unstage a file | git restore --staged <file> | ✓ Yes |
| Undo last commit, keep changes staged | git reset --soft HEAD~1 | Local only |
| Undo last commit, discard changes | git reset --hard HEAD~1 | ⚠ DANGER |
| Safely undo a past commit | git revert <hash> | ✓ Yes — use on shared branches |
⚠️
Golden Rule
Never use git reset --hard or git rebase on commits that have already been pushed to a shared remote branch. Use git revert instead — it adds a new commit that undoes the change, leaving history intact for everyone.
PART 05
Advanced — Rewriting History & Power Features
● Advanced Level
Rebasing re-applies your commits on top of another branch, creating a clean linear history. It is like saying: "pretend I branched off the latest version of main."
main: A──B──C──D
feature: └──E──F
main: A──B──C──D──M
└──E──F──┘
main: A──B──C──D──E'──F'
git switch feature/payment
git rebase main # replay feature commits on top of latest main
# If conflicts arise:
git add resolved_file.py
git rebase --continue # continue to next commit
git rebase --abort # cancel entirely
Interactive Rebase
git rebase -i HEAD~4 # rewrite last 4 commits interactively
# Editor opens showing:
pick a1b2c3d Add login form HTML
pick e4f5g6h Add login CSS
pick i7j8k9l Fix typo in login
pick m1n2o3p Add login backend logic
# Change to squash/fixup to combine commits:
pick a1b2c3d Add login form HTML
squash e4f5g6h Add login CSS # merge into previous
fixup i7j8k9l Fix typo in login # merge + discard message
pick m1n2o3p Add login backend logic
# Keywords: pick | reword | squash | fixup | drop | edit
Cherry-Picking
# Critical bug fix is on develop — you only want that one commit on main
git switch main
git log develop --oneline # find the commit hash
# a3f4c9d Fix: prevent SQL injection in search query
git cherry-pick a3f4c9d # apply just that commit to main
# Cherry-pick a range:
git cherry-pick a3f4c9d..b5e6f7g
git tag -a v1.0.0 -m "First stable release" # annotated tag
git push origin v1.0.0 # push specific tag
git push origin --tags # push all tags
git tag # list all tags
# Annotated tags appear as Releases on GitHub automatically
Reflog — Your Ultimate Safety Net
Git records every single HEAD movement in the reflog — even after git reset --hard. You can almost always recover "lost" commits.
# Disaster: accidentally reset --hard
git reset --hard HEAD~3 # oops — 3 commits gone!
# View reflog to find the lost commits
git reflog
# a3f4c9d HEAD@{0}: reset: moving to HEAD~3
# b5e6f7g HEAD@{1}: commit: Add payment gateway ← this is what we lost
# c8d9e0f HEAD@{2}: commit: Add cart total
# Restore everything!
git reset --hard b5e6f7g
Git Bisect — Find the Bug Commit
git bisect start
git bisect bad # current commit has the bug
git bisect good v1.0.0 # this older tag was fine
# Git checks out a middle commit. Test your app.
git bisect bad # bug is present
# (Git halves the range and checks out another commit)
git bisect good # bug is not present
# Git identifies the exact offending commit!
git bisect reset # return to HEAD when done
Git Hooks
#!/bin/bash
# .git/hooks/pre-commit
if grep -rE "(API_KEY|SECRET_KEY|PASSWORD)\s*=\s*['\"][^'\"]{8,}" --include="*.py" .; then
echo "❌ Potential secret detected! Remove credentials before committing."
exit 1
fi
echo "✅ No secrets detected. Proceeding."
# Make executable:
# chmod +x .git/hooks/pre-commit
GitHub Actions — CI/CD
name: Django CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run Django tests
env:
SECRET_KEY: test-secret-key
DEBUG: True
run: python manage.py test
PART 06
Professional Workflows
Git Flow
main ────────────────────────────────────────►
↑ ↑ ↑
develop ────────┴───────────────┴──────────────┴── ►
↑ ↑ ↑
feature/* ────────┘ ─────┘ ──────┘
↑
hotfix/* ────┘
| Branch | Purpose | Merges Into |
| main | Production-ready code only — never commit directly here | — |
| develop | Integration branch — all features merge here first | main (via release) |
| feature/* | Each new feature in its own branch | develop |
| release/* | Final testing before production release | main + develop |
| hotfix/* | Emergency production fixes | main + develop |
Conventional Commits
# Format: <type>(<scope>): <description>
# Types:
feat: a new feature
fix: a bug fix
docs: documentation only changes
refactor: code restructure, no feature or bug change
test: adding or updating tests
chore: build process, tooling changes
# Real examples:
git commit -m "feat(auth): add JWT token refresh endpoint"
git commit -m "fix(invoice): correct GST calculation for inter-state supply"
git commit -m "refactor(models): extract address fields into AddressMixin"
Useful Git Aliases
git config --global alias.st "status"
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.last "log -1 HEAD --stat"
# Usage:
git st
git lg # beautiful visual branch tree
git undo # safely undo last commit keeping changes
PART 07
Quick Reference Cheat Sheet
git initInitialize new repository
git clone <url>Clone remote repository
git config --listView all configuration
git remote -vShow remote connections
git statusCheck state of all three trees
git add .Stage all changes
git add -pInteractively stage changes
git commit -m 'msg'Create a commit
git pushUpload commits to remote
git pullDownload + merge remote changes
git switch -c feat/xCreate and switch branch
git switch mainSwitch to existing branch
git branch -aList all branches
git merge feat/xMerge branch
git rebase mainRebase onto main
git branch -d feat/xDelete merged branch
git restore <file>Discard working dir changes
git restore --stagedUnstage a file
git reset --soft HEAD~1Undo commit, keep changes
git revert <hash>Safe undo for shared repos
git reflogSee all HEAD movements
git log --oneline --allVisual commit tree
git diffUnstaged line changes
git diff --stagedStaged changes to commit
git blame <file>Who wrote each line
git show <hash>Details of a commit
git tag -a v1.0 -m ''Create annotated tag
git push --tagsPush all tags
git stashSave unfinished work
git stash popRestore latest stash
git cherry-pick <hash>Apply one specific commit
💡
Pro Tip for Multi-Module Projects
For large projects with many modules, use one branch per module — e.g. feature/inventory-module, feature/gst-invoicing. Even on solo projects, this creates clean commit history, makes each module reviewable via PRs, and lets you context-switch safely between modules without losing work.