Introduction
Git is the distributed version control system that underpins almost every modern software team's workflow. Unlike older centralized systems (CVS, SVN), every clone of a Git repository is a full copy of the project's history — there is no single point of failure, and nearly every operation (branching, committing, diffing, browsing history) happens instantly on your local machine, offline.
This guide covers Git the way it's actually used on a production team: the object model that makes branching cheap, the day-to-day commands for branching, merging, and rebasing, how to resolve a real merge conflict under time pressure, the GitHub workflows (fork-and-PR vs. trunk-based with protected branches) teams build around it, and the habits — signed commits, disciplined .gitignoreuse, never rewriting shared history — that keep a team's history trustworthy instead of a source of dread.
Who this is for
- Engineers who know
git add/commit/pushbut freeze at a rebase conflict. - Anyone about to work on a shared feature branch with a team for the first time.
- Developers preparing for Git-heavy technical interviews.
Prerequisites
You should have the following before working through the commands in this guide:
- Git installed locally (this guide shows install steps if you don't have it yet).
- A terminal you're comfortable navigating (cd, ls, a text editor).
- A free GitHub account, plus SSH or HTTPS access configured to it.
- A throwaway local repository or GitHub repo to practice branching and rebasing on — don't practice history rewrites on a repo you can't afford to break.
Git vs. GitHub
Git is the version control tool that runs entirely on your machine. GitHub is a hosted service that stores Git repositories remotely and adds collaboration features — pull requests, code review, issues, Actions. You can use Git without ever touching GitHub; this guide covers both because that's how most teams actually work.Theory
Git Architecture: the object model
Everything Git tracks is stored as one of four object types in the .git/objects database, each addressed by the SHA-1 (or SHA-256, on newer repos) hash of its content:
| Object | What it holds |
|---|---|
blob | The raw contents of a single file (no filename, just bytes) |
tree | A directory listing — maps names/modes to blob or tree hashes |
commit | A pointer to one tree, plus parent commit(s), author, and message |
tag | An annotated, signable pointer to a specific commit |
Because objects are addressed by content hash, identical file content anywhere in the repository — across commits, across branches — is stored exactly once. A commit doesn't store a diff; it stores a full snapshot (a tree), and Git computes diffs on demand by comparing trees. This is why git log -p can be slow on huge repos but git checkout between commits is fast.
The commit graph is a DAG
Commits form a directed acyclic graph (DAG): each commit points backward to its parent(s). A normal commit has one parent; a merge commit has two (or more). This graph structure is what makes operations like git log --graph, git merge-base, and three-way merges possible — Git can always find the common ancestor of any two commits by walking the graph.
A branch is just a pointer
This is the single most important mental model in Git: a branch is not a copy of the code. It is a 41-byte text file in .git/refs/heads/ containing the SHA of one commit. git branch feature-x creates a new pointer at the current commit — an O(1) operation regardless of repo size. Committing on a branch simply moves that pointer forward to the new commit. HEAD is itself a pointer — almost always a symbolic pointer to the branch you have checked out (refs/heads/main, for example), which is how Git knows which branch to advance when you commit.
Why this makes branching cheap
Because a branch is only a pointer, creating one, switching between them, and deleting them are all near-instant, regardless of how large the codebase is — Git never copies files to create a branch.Architecture
Git data moves through four distinct areas as you work. Understanding these boundaries is what makes commands like git add, git commit, and git restore --staged click:
Working directory → staging area → local repository → remote repository, and back
- Working directory — the actual files on disk you edit. Git compares these against the index to compute
git statusandgit diff. - Staging area (the index) — a snapshot-in-progress.
git addcopies working-directory changes here; this is what the next commit will contain. - Local repository — the committed history in
.git, private to your machine until pushed. - Remote repository — a copy hosted elsewhere (GitHub, GitLab) that
git push/git fetch/git pullsynchronize with.
Installation & Initial Setup
Install Git for your platform, then set your identity before making any commits:
sudo apt update
sudo apt install git -y
git --version# Identity used on every commit you make
git config --global user.name "Ranjith R"
git config --global user.email "you@example.com"
# Sane defaults
git config --global init.defaultBranch main
git config --global core.editor "vim"
git config --global pull.rebase false
# Confirm everything
git config --listGlobal vs. local config
--global writes to ~/.gitconfig and applies to every repo for your user. Drop --global inside a specific repository to override just that repo — useful for a work identity that differs from your personal one.Configuration
Repository: init vs. clone
git init creates a brand-new, empty repository (a fresh .git directory) in the current folder — used when starting a project from scratch. git clonecopies an existing remote repository's full history to your machine and automatically wires up origin as the remote name.
# Start a brand-new project
mkdir my-app && cd my-app
git init
# Copy an existing project, including all history
git clone git@github.com:acme/my-app.git
git clone https://github.com/acme/my-app.git
# Clone only recent history (faster for huge repos)
git clone --depth 1 git@github.com:acme/my-app.git.git internals, briefly
Everything Git needs lives inside the .git directory — delete it and the folder is just plain files with no history. Key pieces:
| Path | Purpose |
|---|---|
.git/objects/ | The content-addressed object database (blobs, trees, commits, tags) |
.git/refs/heads/ | One file per local branch, containing a commit SHA |
.git/refs/remotes/ | Read-only tracking refs mirroring remote branches |
.git/HEAD | A symbolic reference to the currently checked-out branch |
.git/config | Repo-local configuration (remotes, aliases, overrides) |
.git/index | The binary staging area file |
.gitignore and remotes
node_modules/
.env
.env.local
dist/
*.log
.DS_Store# View, add, and change remotes
git remote -v
git remote add origin git@github.com:acme/my-app.git
git remote set-url origin git@github.com:acme/my-app-renamed.git
# Useful per-repo aliases
git config alias.st status
git config alias.lg "log --oneline --graph --decorate --all"Commands
Branch
git branch creates or lists pointers; git switch (or the older checkout) moves HEAD to point at one.
git branch # list local branches
git branch feature/checkout-flow # create a branch, stay where you are
git switch feature/checkout-flow # move HEAD to that branch
git switch -c feature/new-thing # create + switch in one step
git checkout -b feature/new-thing # older equivalent of switch -c
git branch -d feature/checkout-flow # delete (safe, blocks if unmerged)
git branch -D feature/checkout-flow # delete (force)
git branch -m old-name new-name # renameMerge: fast-forward vs. three-way
If the target branch hasn't diverged (no new commits since the feature branch was created), Git performs a fast-forward — it just moves the pointer forward, no new commit created. If both branches have diverged, Git creates a three-way merge commit with two parents, using the common ancestor to compute what changed on each side.
git switch main
git merge feature/checkout-flow # fast-forward if possible
git merge --no-ff feature/checkout-flow # always create a merge commit
git merge --abort # bail out of a conflicted mergeRebase
Rebase replays your branch's commits one by one onto a new base commit, producing linear history instead of a merge commit. Interactive rebase additionally lets you reorder, squash, reword, or drop commits before they land.
git switch feature/checkout-flow
git rebase main # replay this branch's commits onto main
git rebase -i HEAD~5 # interactively edit the last 5 commits
git rebase --onto main old-base feature/x # replay a range onto a new base
git rebase --continue # after resolving a conflict mid-rebase
git rebase --abort # bail out, restore original branch stateThe golden rule of rebasing
Never rebase a branch that other people have already pulled or based work on. Rebasing rewrites commit SHAs; anyone with the old history will get duplicate, diverged commits the next time they pull. Rebase freely on your own unpublished feature branches — merge (or a team-agreed force-push protocol) once a branch is shared.Cherry-pick
Apply one specific commit from another branch onto your current branch:
git cherry-pick a1b2c3d # apply a single commit here
git cherry-pick a1b2c3d..f9e8d7c # apply a range (exclusive of first)
git cherry-pick --continue # after resolving a conflict
git cherry-pick --abortStash
Shelve uncommitted work temporarily without committing it, e.g. to switch branches:
git stash push -m "wip: payment retry logic"
git stash list
git stash show -p stash@{0}
git stash pop # re-apply and remove from the stash list
git stash apply # re-apply but keep it in the stash list
git stash drop stash@{0}
git stash -u # include untracked filesReset: --soft, --mixed, --hard
| Mode | HEAD | Staging area | Working directory |
|---|---|---|---|
--soft | moved | unchanged (kept staged) | unchanged |
--mixed (default) | moved | reset to match new HEAD | unchanged |
--hard | moved | reset | reset — uncommitted work is lost |
git reset --soft HEAD~1 # undo last commit, keep changes staged
git reset HEAD~1 # (--mixed) undo commit + staging, keep edits
git reset --hard HEAD~1 # discard the commit and all its changes entirely
git reset HEAD~1 -- file.txt # unstage a single fileRevert
Unlike reset, git revertdoesn't rewrite history — it creates a new committhat undoes a previous one. This makes it the safe choice for undoing something that's already been pushed and shared.
git revert a1b2c3d # create a commit undoing a1b2c3d
git revert --no-commit a1b2c3d..HEAD # revert a range, stage without committing yetTag
A lightweight tag is just a named pointer to a commit. An annotated tag is a full Git object with its own message, tagger, date, and optional GPG signature — use annotated tags for releases.
git tag v1.2.0 # lightweight
git tag -a v1.2.0 -m "Release 1.2.0" # annotated
git tag -s v1.2.0 -m "Release 1.2.0" # annotated + GPG-signed
git push origin v1.2.0 # push a single tag
git push origin --tags # push all tags
git tag -d v1.2.0 && git push origin :refs/tags/v1.2.0 # delete local + remoteExamples
A full feature branch lifecycle, from creation to a merged pull request:
Branch off main
git switch main && git pull, thengit switch -c feature/checkout-flowto start isolated work.Commit in small, logical chunks
git add -pto stage hunks selectively, thengit commit -m "feat: add coupon field to checkout"— small commits make review and futuregit bisectruns far easier.Stay current with main
Periodicallygit fetch originthengit rebase origin/mainto replay your commits on top of the latest main, resolving small conflicts early instead of one large one at the end.Push and open a pull request
git push -u origin feature/checkout-flow, then open a PR on GitHub againstmainand request review.Address review feedback
Push additional commits, orgit commit --amend/git rebase -ito tidy history before merge if the team prefers a clean log.Merge and clean up
Merge via GitHub (squash, rebase, or merge commit per team convention), then locallygit switch main && git pull && git branch -d feature/checkout-flow.
Screenshots
git log --oneline --graph --decorate --all showing a feature branch merged back into main
Screenshot placeholder
GitHub pull request view with a required review and a green CI check before the Merge button unlocks
Screenshot placeholder
Real World Example
Scenario: two engineers both edited src/lib/pricing.ts — one on feature/discount-codes, one already merged to main. Rebasing the feature branch produces a conflict.
Rebase surfaces the conflict
git rebase origin/mainstops mid-replay:CONFLICT (content): Merge conflict in src/lib/pricing.ts.Inspect what's conflicted
git statuslists the file as "both modified". Opening it shows Git's conflict markers:Read the conflict markers
<<<<<<< HEADthrough=======is the incoming commit being replayed;=======through>>>>>>> a1b2c3dis the commit already on main. Both edits are kept side by side until a human decides.Resolve, verifying with a diff
Edit the file to keep the correct combined logic, remove all three marker lines, thengit diffto confirm the result actually makes sense (not just that markers are gone) beforegit add src/lib/pricing.ts.Continue the rebase
git rebase --continue— repeat for any further conflicting commits in the replay, then run the test suite before force-pushing.Force-push the rewritten branch safely
git push --force-with-lease origin feature/discount-codes— unlike a plain--force, this refuses to push if someone else has pushed to the branch since your last fetch, preventing you from silently discarding a teammate's work.
A merge tool can help on messier conflicts
For conflicts spanning many hunks,git mergetool (wired to VS Code, meld, or similar) shows base/local/remote side by side — often faster than reading raw markers on a large diff.Common Issues
Detached HEAD state
Checking out a specific commit or tag (git checkout a1b2c3d) puts you in detached HEAD — commits made here belong to no branch and can be lost once you switch away. Fix it immediately with git switch -c recovery-branch to give those commits a real branch to live on, or use git log --all --oneline plus git reflog to find and rescue them if you already switched away.Accidentally committed directly to main
Don't push. Create a branch at the current position (git branch feature/my-fix), then move main back with git reset --hard origin/main, and switch to feature/my-fix to continue and open a PR properly.Force-push overwrote a teammate's commits
A plaingit push --force replaces the remote branch unconditionally, even if someone else pushed in between. Prevent it going forward with git push --force-with-lease, which fails safely in that situation. To recover lost commits, the teammate's local git reflog still has the SHA — cherry-pick or reset back to it and re-push.A large file was committed by mistake
Removing it in a new commit doesn't shrink the repo — it's still in history. For a file not yet pushed,git reset --soft HEAD~1, unstage it, and recommit. For history already pushed, use git filter-repo (preferred over the legacy filter-branch) to strip it from all history, then have every collaborator re-clone, since this rewrites every downstream SHA.Merge conflict markers accidentally committed
If<<<<<<</=======/>>>>>>> lines slip into a commit, it usually means git addran before the conflict was fully resolved. Fix the file, re-add, and amend if it hasn't been pushed yet; otherwise commit a follow-up fix — add a pre-commit hook that greps for ^<<<<<<< to catch this before it happens again.Best Practices
- Write commit messages in the imperative mood ("add", not "added") with a short summary line and a blank line before any body detail.
- Commit small, logically complete units of work — it makes review,
git bisect, and reverts precise instead of all-or-nothing. - Pull with rebase (
git pull --rebase) on your own feature branches to avoid noisy merge-of-merge commits in history. - Never rewrite history on a branch others have already pulled — see the golden rule of rebasing.
- Keep
mainalways deployable; do all work on short-lived feature branches merged via reviewed pull requests. - Tag every release with an annotated tag so
git describeand changelogs have a stable reference point.
Security Hardening
- Sign your commits —
git config --global commit.gpgsign truewith a GPG or SSH signing key gives GitHub a verified badge and proves authorship. - Protect the main branch — require pull requests, passing status checks, and at least one approving review before merge; disallow direct pushes and force-pushes on GitHub's branch protection rules.
- Never commit secrets — API keys,
.envfiles, and credentials belong in a secrets manager, not source control; scan history with tools likegitleaksortrufflehogbefore it ships. - Maintain .gitignore discipline — commit a sensible
.gitignorefrom day one; a secret committed once still exists in history even after a later commit deletes it. - Use a credential helper, not plaintext tokens —
git config --global credential.helper(or an OS keychain) stores credentials securely; never paste a personal access token into a remote URL that gets committed anywhere.
Interview Questions
Cheat Sheet
# --- Setup ---
git init ; git clone <url>
git config --global user.name "..." ; git config --global user.email "..."
# --- Status / History ---
git status
git log --oneline --graph --decorate --all
git diff ; git diff --staged
# --- Stage / Commit ---
git add <file> ; git add -p ; git add -A
git commit -m "feat: ..." ; git commit --amend
# --- Branching ---
git branch ; git switch -c <name> ; git branch -d <name>
# --- Sync ---
git fetch ; git pull --rebase ; git push -u origin <branch>
git push --force-with-lease # safe force-push after a rebase
# --- Merge / Rebase ---
git merge <branch> ; git merge --no-ff <branch>
git rebase main ; git rebase -i HEAD~5 ; git rebase --continue|--abort
# --- Undo ---
git restore <file> ; git restore --staged <file>
git reset --soft|--mixed|--hard HEAD~1
git revert <commit>
# --- Stash / Cherry-pick / Tag ---
git stash push -m "wip" ; git stash pop
git cherry-pick <commit>
git tag -a v1.0.0 -m "Release" ; git push origin --tags
# --- Recovery ---
git reflog # find "lost" commits after a reset/rebase gone wrongSummary
Git's entire feature set follows from one idea: content-addressed objects (blobs, trees, commits) linked into a DAG, with branches as cheap, movable pointers into that graph. Once that clicks, merge, rebase, cherry-pick, reset, and revert all read as different ways of moving pointers and replaying or combining snapshots rather than separate tricks to memorize. Layer a GitHub workflow — protected main, required reviews, feature branches — on top, and the same primitives scale from a solo project to a large team without the history turning into chaos.
Resources
- Official Git documentation and the free Pro Git book —
git-scm.com - GitHub Docs — pull requests, Actions, branch protection —
docs.github.com - Interactive branching visualizer —
learngitbranching.js.org - Atlassian's Git tutorials (merge vs. rebase, workflows) —
atlassian.com/git - Oh Shit, Git!?! — plain-English fixes for common mistakes —
ohshitgit.com