What is Git?
Git is a distributed version control system — it records snapshots of your project over time so you can review history, undo mistakes, and collaborate without overwriting each other's work. "Distributed" means every clone is a full copy of the repository, including its entire history; you can commit, branch, and browse history completely offline.
A repository (repo) is a project folder that Git is tracking. It lives in a hidden .git directory that stores every commit, branch, and configuration.
The Three Areas: Working Tree, Staging, Repository
Understanding Git's three areas removes almost all early confusion:
- Working tree — the actual files you edit on disk.
- Staging area (index) — a holding area where you assemble exactly what the next commit will contain.
- Repository — the committed history stored in
.git.
You edit files (working tree), pick which changes to include with git add (staging), then seal them into history with git commit. A commit is an immutable snapshot with an author, timestamp, message, and a unique SHA hash.
Why staging exists
Staging lets you craft focused commits. If you fixed a bug and also tweaked docs, you can stage and commit them separately — one clean change per commit makes history readable and easy to revert.
Core Commands
# Start tracking a new project
git init
# Copy an existing remote repo to your machine
git clone https://github.com/org/repo.git
# See what changed and what's staged
git status
# Stage changes for the next commit
git add file.js # one file
git add . # everything in current dir
# Record staged changes with a message
git commit -m "Add login validation"
# Stage tracked changes AND commit in one step
git commit -am "Fix typo"
# Review history
git log --oneline --graph --all
# See unstaged vs staged changes
git diff # working tree vs staging
git diff --staged # staging vs last commit
Branching & Merging
A branch is a movable pointer to a commit — a cheap, independent line of work. You create a branch for each feature or fix so the main line stays stable, then merge it back when done.
# Create and switch to a new branch
git switch -c feature/login # modern (git 2.23+)
git checkout -b feature/login # older, equivalent
# List and switch branches
git branch
git switch main
# Merge feature back into main
git switch main
git merge feature/login
# Delete a merged branch
git branch -d feature/login
A fast-forward merge just moves the pointer forward when main hasn't changed. When both branches advanced, Git creates a merge commit that ties the two histories together.
Remotes: push, pull, fetch
A remote is a shared copy of the repo hosted elsewhere (GitHub, GitLab). The default remote is named origin.
# See configured remotes
git remote -v
# Download remote changes WITHOUT merging (safe to inspect first)
git fetch origin
# Fetch AND merge into current branch
git pull
# Upload local commits, setting upstream the first time
git push -u origin feature/login
git push # subsequent pushes
| Command | Direction | Touches working tree? |
|---|---|---|
git fetch | Remote → local repo | No (just updates refs) |
git pull | Remote → local + merge | Yes (fetch + merge) |
git push | Local → remote | No |
Merge vs Rebase
Both integrate changes from one branch into another, but differently. Merge preserves history exactly and adds a merge commit. Rebase replays your commits on top of another branch, producing a clean, linear history — but it rewrites commit hashes.
# Merge: keeps both histories, adds a merge commit
git switch feature
git merge main
# Rebase: move feature's commits on top of latest main
git switch feature
git rebase main
# resolve any conflicts, then:
git rebase --continue
| Aspect | Merge | Rebase |
|---|---|---|
| History | Non-linear, true record | Linear, tidy |
| Commit hashes | Preserved | Rewritten |
| Safe on shared branches | Yes | No — never rebase pushed history |
| Best for | Integrating shared branches | Cleaning up your own local work |
The golden rule of rebase
Never rebase commits that others may have already pulled. Rewriting shared history forces everyone else into painful conflicts. Rebase only your own local, unpushed commits.
Resolving Conflicts
A conflict happens when two branches change the same lines. Git pauses and marks the file with conflict markers; you edit it to the correct result, then stage and continue.
<<<<<<< HEAD
const timeout = 30; // your change
=======
const timeout = 60; // incoming change
>>>>>>> feature/login
# After editing to the final version, remove the markers, then:
git add config.js
git commit # (for merge) or: git rebase --continue
# Give up and go back to before the merge
git merge --abort
.gitignore
A .gitignore file lists patterns Git should not track — build artifacts, dependencies, secrets, and editor files. Keep it committed so the whole team shares it.
# dependencies
node_modules/
# build output
dist/
build/
# secrets and env
.env
*.key
# OS / editor cruft
.DS_Store
.vscode/
Ignore rules only affect untracked files. If a file is already committed, add it to .gitignore and run git rm --cached file to stop tracking it.
Common Team Workflows
Feature Branch Workflow
Create a branch per task, commit there, open a pull request, merge into main after review. This is the foundation of the others.
GitHub Flow
A lightweight flow: main is always deployable. Branch off main, open a PR, get review, merge, deploy. Ideal for continuous delivery and most teams in 2026.
Git Flow
A heavier model with long-lived main and develop branches plus feature/, release/, and hotfix/ branches. Suited to versioned software with scheduled releases; usually overkill for web apps that deploy continuously.
Pull Requests
A pull request (PR, or merge request) proposes merging one branch into another. It's where code review happens: teammates comment, CI runs tests, and once approved the branch is merged. The typical loop:
git switch -c feature/checkout
# ... make changes, commit ...
git push -u origin feature/checkout
# open a PR on GitHub, address review feedback,
# push follow-up commits, then merge (squash is common)
Undoing Changes
Git gives you several tools to undo; the right one depends on whether the work is committed and whether it's already been shared.
# Discard unstaged changes to a file
git restore file.js
# Unstage a file (keep the edits)
git restore --staged file.js
# Move HEAD back, keep changes staged/unstaged
git reset --soft HEAD~1 # undo commit, keep staged
git reset HEAD~1 # undo commit, keep in working tree
git reset --hard HEAD~1 # DANGER: discard commit AND changes
# Safely undo a PUBLISHED commit by adding an inverse commit
git revert <commit>
# Temporarily shelve work to switch tasks
git stash
git stash pop # reapply and remove from stash
git stash list
| Command | Use when |
|---|---|
git restore | Throw away uncommitted edits |
git reset | Undo LOCAL commits not yet pushed |
git revert | Undo a commit that's already shared |
git stash | Set work aside without committing |
Tags
A tag marks a specific commit, usually to label a release. Annotated tags store a message and author and are preferred for releases.
git tag v1.4.0 # lightweight
git tag -a v1.4.0 -m "Release 1.4.0" # annotated
git push origin v1.4.0 # tags aren't pushed by default
git push origin --tags # push all tags
Practice Exercises
- Initialize a new repo, create three commits, and view the history with
git log --oneline --graph. - Create a feature branch, make a change, and merge it back into main; observe whether it was a fast-forward.
- Deliberately create a merge conflict by editing the same line on two branches, then resolve it.
- On a local feature branch, rebase onto an updated main and compare the resulting history to a merge.
- Make a bad commit, then undo it two ways: with
git reset(local) and withgit revert(as if shared). - Stash in-progress work, switch branches to fix an urgent bug, then pop the stash and continue.
- Create an annotated tag for a release and push it to the remote.