● Beginner ● Intermediate ● Advanced
⎇ Complete Practical Tutorial

Git & GitHub
Mastery Guide

From understanding how version control actually works under the hood, to writing production CI/CD pipelines — everything in one place.

7
Parts
30+
Topics
50+
Code Examples
3
Skill Levels
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

TypeDescriptionExamples
LocalTracks changes only on your own machineRCS
CentralizedSingle server holds all history; clients check out filesSVN, CVS, Perforce
DistributedEvery client has the full repository including historyGit, 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
git add
──▶
git restore
📦
Staging Area
the Index
git commit
──▶
git reset
🏛️
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.

Why Staging Is Powerful
# 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 directory structure
.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

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.

The Four Git Object Types
Commit Object │ ├── message: "Add Customer model" │ ├── author: Jobin Jose │ ├── timestamp: 2026-03-06 │ └── pointer to: Tree Object + Parent CommitTree Object (represents a directory) │ ├── Blob → README.md │ ├── Blob → models.py │ └── Tree → app/ │ └── Blob → app/views.pyBlob 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.

# HEAD points to a branch, which points to a commit main: A──B──C──DHEADmainD # After: git switch feature/login main: A──B──C──D feature: └──E──FHEADfeature/loginF

Understanding git status & git diff

Annotated git status output
$ 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
git diff — line-level changes in your working directory
@@-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)
The three diff commands
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

Install Git
# 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
First-time configuration
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-by-step first 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 stuffFix: null pointer crash when user email is empty
updateRefactor: extract payment logic into PaymentService
asdfFeat: add GST invoice PDF export with e-way bill
wipWIP: incomplete — do not merge (inventory module)

Connecting to GitHub

Link local repo 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
Clone an existing repository
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.

Branch commands
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
Full feature branch workflow
# 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

# Fast-Forward merge (no new commits on main — clean linear history): main: A──B feature: C──D # result: A──B──C──D (main pointer just moves forward) # 3-Way merge (both branches have new commits): main: A──B──E──F feature: └──C──D # result: A──B──E──F──M (M is the merge commit) └──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.

What a conflict looks like
<<<<<<< HEAD login_field = "email" # your current branch ======= login_field = "phone" # branch being merged in >>>>>>> feature/phone-login
Step-by-step conflict resolution
# 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

Stash commands
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

.gitignore for Django + Flutter project
# 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)

  1. Create a feature branch: git switch -c fix/navbar-bug
  2. Make changes, commit, and push: git push origin fix/navbar-bug
  3. On GitHub → click "Compare & Pull Request"
  4. Write a clear description of what changed and why
  5. Request reviewers and address their feedback comments
  6. After approval → click "Merge Pull Request"

Undoing Changes

SituationCommandSafe for Shared?
Discard working dir changesgit restore <file>✓ Yes
Unstage a filegit restore --staged <file>✓ Yes
Undo last commit, keep changes stagedgit reset --soft HEAD~1Local only
Undo last commit, discard changesgit reset --hard HEAD~1⚠ DANGER
Safely undo a past commitgit 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."

# BEFORE: feature diverged from main main: A──B──C──D feature: └──E──F # AFTER MERGE (creates merge commit M): main: A──B──C──D──M └──E──F──┘ # AFTER REBASE (linear history, no merge commit): main: A──B──C──D──E'──F' # E' and F' are new commits — same changes, different hashes
Rebase command
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

Squash, reorder, and clean up commits
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

Apply a specific commit to your branch
# 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

Tags & Releases

Tagging versions
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.

Recovering 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

Binary search through history
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

Pre-commit hook — block secrets from being committed
#!/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

.github/workflows/django-ci.yml
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 ────────────────────────────────────────► production only ↑ ↑ ↑ develop ────────┴───────────────┴──────────────┴── ► integration ↑ ↑ ↑ feature/* ────────┘ ─────┘ ──────┘ feature workhotfix/* ────┘ urgent production fix
BranchPurposeMerges Into
mainProduction-ready code only — never commit directly here
developIntegration branch — all features merge here firstmain (via release)
feature/*Each new feature in its own branchdevelop
release/*Final testing before production releasemain + develop
hotfix/*Emergency production fixesmain + develop

Conventional Commits

Conventional Commit format
# 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

Add to ~/.gitconfig
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

⚙️ Setup
git initInitialize new repository
git clone <url>Clone remote repository
git config --listView all configuration
git remote -vShow remote connections
📅 Daily Workflow
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
🌿 Branches
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
↩️ Undoing
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
🔍 Inspect
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
🏷️ Tags & Stash
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.