Version control, branching, rebase, remotes, and the beautiful graph underneath it all.
Created by Linus Torvalds in 2005 to manage the Linux kernel. Now the world's most-used VCS.
Git doesn't store diffs — it stores snapshots. Each commit is a complete picture of your project at that moment, referenced by a hash.
Starting a new repo and telling Git who you are.
# Create a new repository in current directory git init # Git creates a hidden .git/ folder: # .git/ # ├── HEAD ← points to current branch # ├── config ← repo-local settings # ├── objects/ ← all your data (blobs, trees, commits) # └── refs/ ← branches and tags # ───────────────────────────────────────── # Tell Git who you are (stored in ~/.gitconfig) git config --global user.name "Ada Lovelace" git config --global user.email "[email protected]" # Set your preferred editor (for commit messages) git config --global core.editor "vim" # Useful: always rebase on pull (recommended) git config --global pull.rebase true # Check your config git config --list # Scope: --global (user), --local (repo), --system
Your name and email are baked into every commit permanently. Set them correctly before your first commit — changing them later in shared repos is painful.
git clone <url> is just git init + fetch + checkout, already configured with the remote.
Git has three distinct zones. Understanding them is the key to everything.
Moving work from your editor into permanent history.
# Stage a specific file git add src/main.py # Stage everything in current directory git add . # Stage interactively — pick hunks, not whole files git add -p # See what's staged vs unstaged git status git diff # unstaged changes git diff --staged # staged changes (what will be committed) # ───────────────────────────────────────── # Commit staged files git commit -m "feat: add user authentication" # Stage all tracked files AND commit in one step git commit -am "fix: correct off-by-one in parser" # Amend the last commit (before pushing!) git commit --amend # Open editor for multi-line commit message git commit
Interactively stage hunks within a file. Make one commit per logical change — even if you edited multiple things in one session. Clean history = clean team.
type: short summary (≤72 chars)Rewrites the last commit (new hash!). Safe to use before pushing. After pushing, coordinate with your team first.
A branch is just a lightweight pointer to a commit. Creating one costs almost nothing.
feature/user-loginfix/null-pointer-crashchore/update-depsrelease/2.4.0Creating a branch is instantaneous and costs 41 bytes. There is zero reason not to branch for every feature, fix, or experiment.
HEAD is a pointer to your current branch, which itself points to a commit. Moving between branches just updates HEAD — your files update to match.
Exploring the commit graph. Pretty-print makes it actually readable.
# Basic log git log # One line per commit git log --oneline # Pretty graph — the most useful alias you'll ever set git log --oneline --graph --all --decorate # Example output: * e5f6a7b (HEAD -> feature/login) add JWT validation * d4e5f6c add login form | * 3c4d5e6 (main) hotfix: sanitise inputs |/ * 2b3c4d5 initial commit # Set a permanent alias for the above git config --global alias.lg \ "log --oneline --graph --all --decorate" # Then just: git lg # Search commits by message git log --grep="fix" # Search commits by code change (pickaxe) git log -S "function parseUser" # Show changes in a commit git show d4e5f6c
Always use --graph --all. Without it you only see the current branch. The graph reveals the true structure of your DAG.
main..feature — commits in feature not in mainHEAD~3 — 3 commits back from HEADHEAD^ — first parent of HEADgit blame file.py shows which commit last touched each line. Invaluable for understanding why code exists. Not for naming and shaming!
Git history is not a line. It's a graph. Every commit points to its parent(s). This is the foundation of everything.
Every commit's hash depends on its content AND its parent's hash. Change one thing, change everything after it.
Never rewrite history that has been pushed and shared. When you change a commit, everyone who built on top of it gets a divergent branch they must manually reconcile.
Git is migrating from SHA-1 to SHA-256 for security. SHA-256 hashes are 64 hex chars. You may see both in modern repos. The integrity guarantee is the same.
Rebase moves your branch to start from a different point, creating new commits with the same changes but fresh hashes.
main / develop branch directlyMerge preserves the exact history as it happened. Rebase creates a cleaner linear story. Most teams prefer rebase for feature branches + a single merge commit at the end.
git rebase -i lets you squash, reorder, edit or drop commits before sharing your work.
# Interactively rebase the last 4 commits git rebase -i HEAD~4 # Editor opens with a list like this: pick a1b2c3d feat: add login form pick e4f5g6h fix: typo in label pick i7j8k9l fix: another typo pick m0n1o2p test: add login tests # Change "pick" to one of: # p pick — keep commit as-is # r reword — keep changes, edit message # e edit — pause and amend the commit # s squash — meld into previous commit # f fixup — squash, discard this message # d drop — remove the commit entirely # Clean version: squash the typo fixes pick a1b2c3d feat: add login form f e4f5g6h fix: typo in label f i7j8k9l fix: another typo pick m0n1o2p test: add login tests # Result: 2 clean commits instead of 4
git rebase -isquash opens the editor to combine both commit messages. fixup silently discards the message of the squashed commit — faster for typo fixes.
git add <file>git rebase --continuegit rebase --abortYour local repo is complete. Remotes are just other repos you sync with.
git clone <url> — full copy + remote configuredgit fetch — download remote changes, don't mergegit pull — fetch + merge (or rebase) into current branchgit push — upload local commits to remotegit fetch is always safe — it only updates your remote-tracking branches. git pull also modifies your working branch. When in doubt, fetch first, inspect, then merge.
git push -u origin feature/login — the -u sets the upstream so future git push works without arguments.
A Merge Request (GitLab) / Pull Request (GitHub) is a code review + merge in one workflow. Fast-forward keeps history clean.
A good commit message is a letter to future-you (and your teammates). Make it count.
# ✅ GOOD commit message feat(auth): add JWT refresh token rotation Refresh tokens now rotate on each use to prevent token replay attacks. Old tokens are invalidated immediately after a successful refresh. Closes #482 See: https://datatracker.ietf.org/doc/html/rfc6749 # ──────────────────────────────────────── # ❌ BAD commit messages fix stuff wip asdfgh updated files it works now final final FINAL ok seriously final this time
type(scope): summaryfeat!: — breaking change (major bump)git log --grep becomes usefulgit bisect finds bugs fasterTemporarily shelve work-in-progress so you can switch context without committing.
# Save current changes to the stash stack git stash push # (shorthand — same thing) git stash # Give it a meaningful name git stash push -m "WIP: half-done auth refactor" # Include untracked files too git stash push -u # List all stashes git stash list # stash@{0}: WIP: half-done auth refactor # stash@{1}: On main: quick experiment # Restore most recent stash (and remove from stack) git stash pop # Apply without removing from stack git stash apply stash@{1} # Apply stash to a new branch git stash branch feature/auth-wip # Discard a stash git stash drop stash@{0} git stash clear # drop ALL
git stash push -m "WIP login"git switch main — fix the bug, commit, pushgit switch feature/logingit stash pop — back to workgit reset HEAD~1 when returning.Under the hood, stash creates real commits in a special ref. You can even git show stash@{0} to inspect what's saved.
Check out multiple branches simultaneously into separate directories — no stashing needed.
# Add a new worktree for a branch git worktree add ../myapp-hotfix hotfix/critical-bug # Add worktree for a NEW branch git worktree add -b feature/payments ../myapp-payments # List all active worktrees git worktree list # /home/ada/myapp a1b2c3d [main] # /home/ada/myapp-hotfix d4e5f6g [hotfix/critical-bug] # /home/ada/myapp-payments h7i8j9k [feature/payments] # Work in each directory independently # cd ../myapp-hotfix && vim src/bug.py && git commit # cd ../myapp-payments && vim src/pay.py && git commit # Remove a worktree when done git worktree remove ../myapp-hotfix # All worktrees share the same .git/ objects # Commits from any worktree appear in all
.git/ — commits are instantly visible everywhereFrom the first commit to rewriting history like a pro.
fetch (safe) vs pull (merges). Push -u to set upstream. MRs / PRs are code review + merge. Rebase before merge for fast-forward.
git stash for quick context switches. git worktree for parallel branch work. git stash branch to promote a stash to a branch.
Conventional commits: feat/fix/chore. Explain why in the body. Good history is a gift to future teammates — and future you.