A practical, teaching-style Q&A guide for Git. Each question gives a plain-language explanation followed by real command sequences a beginner can follow along with.
- Fundamentals
- Working Directory, Staging, Commits
- Branches, Merge, Rebase
- Resetting, Reverting, Recovery
- Stash, Cherry-pick, Reflog
- Tags and Releases
- Remotes, Push, Pull
- Workflows and Code Review
- Hooks, Submodules, LFS
- Worktrees, Bisect, Blame
- Configuration, Aliases, Performance
- Common Scenarios
Answer:
Git is a distributed version control system. It tracks every change you make to a set of files, lets you create branches to work on multiple things in parallel, and lets you collaborate with others. "Distributed" means every clone is a full copy of the project history — you can commit, branch, and view history without a network.
git --version
# git version 2.43.0Answer:
In a centralized system (SVN, CVS), there is one server that holds the history. To commit, you must talk to it. If the server is down or unreachable, you cannot work.
In a distributed system (Git, Mercurial), every clone holds the full history. You can commit, look at history, and create branches offline. Pushing and pulling syncs your clone with a remote.
Answer:
Git tracks files across three "trees":
| Tree | Description |
|---|---|
| Working directory | Your actual files on disk that you edit |
| Index (staging) | A snapshot of what will go into the next commit |
| HEAD | The latest commit on the current branch |
A typical workflow moves changes between them:
# 1. Edit a file
echo "hello" > note.txt
# 2. Stage it (working dir -> index)
git add note.txt
# 3. Commit it (index -> HEAD)
git commit -m "add note"
# Inspect the trees
git status # working dir vs index vs HEAD
git diff # working dir vs index
git diff --cached # index vs HEADAnswer:
Git stores everything as one of four object types in .git/objects/:
| Object | Purpose |
|---|---|
| blob | The contents of a file (no name, no permissions) |
| tree | A directory: a list of blobs and other trees with names and modes |
| commit | A pointer to a tree, plus parent commit(s), author, message |
| tag | A named pointer to a commit (annotated tags only) |
Every object is identified by a SHA-1 hash of its content.
git cat-file -t HEAD # type: commit
git cat-file -p HEAD # show the commit's content
git cat-file -p HEAD^{tree} # show the root treeAnswer:
A commit object contains:
- A reference to the root tree (a snapshot of the entire project at that point).
- Parent commit references (one for normal commits, two for merges, zero for the initial commit).
- Author and committer (name, email, timestamp).
- The commit message.
git show --no-patch HEAD
# commit abc123...
# Author: Alice <alice@example.com>
# Date: Mon May 5 10:00:00 2025 +0000
#
# Add noteAnswer:
- HEAD is a pointer to the current commit you have checked out. Usually it points to a branch name, which then points to a commit.
- main (or master) is the default branch name in many repositories — just a label pointing to a commit.
- origin is the default name for the remote you cloned from.
origin/mainis your local view of the remote'smainbranch.
cat .git/HEAD # ref: refs/heads/main
git branch # which branch HEAD points to
git remote -v # remote URLsAnswer:
git status summarizes which files are modified, staged, or untracked.
git diff shows the actual line-level changes.
git status # full status report
git status -s # short version
git diff # working dir changes (not yet staged)
git diff --cached # staged changes
git diff HEAD # all uncommitted changes (staged + unstaged)
git diff main..feature # changes on feature not on mainAnswer:
You can stage entire files, individual files, or even individual chunks (hunks) of a file.
git add file.txt # stage one file
git add src/ # stage a directory
git add . # stage everything in current dir
git add -A # stage everything in the repo
git add -p # interactively stage hunks (very useful)
git restore --staged file.txt # unstage a filegit add -p walks you through each change and asks "stage this? [y/n/s/...]". Great for splitting unrelated edits into separate commits.
Answer:
A common convention is Conventional Commits:
type(scope): short summary
Optional longer body explaining the why.
Footer: BREAKING CHANGE, references, etc.
Examples:
feat(auth): add OAuth2 login
fix(billing): handle null discount in invoice total
refactor(orders): extract pricing to a service
docs: update README with deploy steps
chore: bump dependencies
Rules of thumb: 50-character subject, imperative mood ("add" not "added"), explain why in the body if not obvious.
Answer:
git commit --amend rewrites the most recent commit. Use it to fix a typo in the message or to add a forgotten file.
git add forgotten-file.txt
git commit --amend # opens editor for new message
git commit --amend --no-edit # keep the same messageCaution: amending changes the commit's SHA. Do not amend commits that have already been pushed to a shared branch.
Answer:
git branch # list local branches
git branch -a # include remote-tracking branches
git switch -c feature/login # create and switch (new style)
git checkout -b feature/login # create and switch (older style)
git switch main # switch back
git branch -d feature/login # delete (safe; refuses if unmerged)
git branch -D feature/login # delete (force)
git branch -m old-name new-name # renameAnswer:
A fast-forward merge happens when the target branch (e.g., main) has not moved since the feature branch started. Git just moves main's pointer forward to the feature's tip — no merge commit needed.
A three-way merge happens when both branches have new commits. Git combines the changes from both sides and creates a new merge commit with two parents.
git merge feature/login # fast-forward if possible
git merge --no-ff feature/login # always create a merge commit
git merge --ff-only feature/login # refuse if not fast-forward--no-ff keeps the feature branch visible in the history graph, which many teams prefer.
Answer:
Merge preserves history exactly as it happened. The merge commit shows where two lines of work joined.
Rebase rewrites your branch's commits as if they had been made on top of the latest target branch. The result is a clean linear history.
# Merge approach
git switch feature
git merge main # creates a merge commit on feature
# Rebase approach
git switch feature
git rebase main # replay feature commits on top of main
# resolve any conflicts as Git pauses; then:
git rebase --continue
# or to abort:
git rebase --abortRule of thumb: rebase your local branch to keep history clean before merging it; never rebase a branch others are working on.
Answer:
Interactive rebase lets you reorder, edit, squash, or drop commits before they reach the shared branch.
git rebase -i HEAD~5Git opens an editor showing the last 5 commits:
pick a1b2c3 add login form
pick d4e5f6 fix typo
pick g7h8i9 add password reset
pick j1k2l3 wip
pick m4n5o6 polish login UI
Change pick to:
reword— keep the commit, change the messageedit— pause to amend the commitsquash— combine into the previous commit (asks for a new message)fixup— like squash but discards this commit's messagedrop— remove the commit entirely
You can also reorder lines.
Answer:
When Git cannot automatically combine changes, it inserts conflict markers in the file:
<<<<<<< HEAD
const port = 3000;
=======
const port = 8080;
>>>>>>> feature/port
To resolve:
# 1. Edit each conflicted file, choose the right content,
# remove the <<<<<<< ======= >>>>>>> markers.
# 2. Stage the resolved files.
git add src/app.js
# 3. Continue the operation.
git commit # for merge
git rebase --continue # for rebaseUseful tools:
git status # shows files with conflicts
git diff # shows the conflict regions
git mergetool # opens a GUI tool
git checkout --ours file # take our version
git checkout --theirs file # take their versionAnswer:
git fetchdownloads new commits from the remote into your local remote-tracking branches (origin/main, etc.) but does not change your working branch.git pullisgit fetch+git merge(orgit pull --rebaseisgit fetch+git rebase). It updates your current branch.
git fetch # safe; just refreshes origin/* refs
git log main..origin/main # see what would be pulled
git pull # default: fetch + merge
git pull --rebase # fetch + rebase (linear history)A good default is git config --global pull.rebase true.
Answer:
git reset moves the current branch pointer to a specific commit. The flag controls what happens to the index and working directory.
| Flag | Branch | Index | Working dir | Use case |
|---|---|---|---|---|
--soft |
moved | unchanged | unchanged | Undo a commit but keep changes staged |
--mixed (default) |
moved | reset to commit | unchanged | Undo commit and unstage changes |
--hard |
moved | reset | reset | Throw everything away (destructive!) |
git reset --soft HEAD~1 # undo last commit, keep changes staged
git reset HEAD~1 # undo last commit, keep changes in working dir
git reset --hard HEAD~1 # delete the last commit AND its changesAnswer:
git revert creates a new commit that undoes the changes of a previous commit. It does not rewrite history.
git revert abc1234 # creates "Revert ... abc1234" commit
git revert HEAD # revert the most recent commit
git revert -n abc1234 # stage but don't commit yetThis is the safe way to undo something on a shared branch.
Answer:
| Tool | Effect | Safe on shared branches? |
|---|---|---|
git revert |
Adds a new commit that undoes | Yes |
git reset |
Moves the branch pointer (rewrites) | No (force-push needed) |
Use revert to undo a commit that has been pushed. Use reset to clean up your local commits before pushing.
Answer:
git reflog records every place HEAD has been. If you accidentally reset, rebased, or deleted a branch, the commits are still there for ~90 days.
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: my important work <- this is what I want back
# ...
git checkout def5678 # detach HEAD onto the lost commit
git switch -c rescue # create a branch from itAnswer:
These are newer commands (Git 2.23+) that split git checkout into clearer halves.
git switchchanges branches.git restorerestores files.
git switch main # switch branch
git switch -c feature # create and switch
git restore file.txt # discard working-dir changes
git restore --staged file.txt # unstage
git restore --source=HEAD~1 file.txt # restore from a specific commitThe old git checkout still works but is overloaded.
Answer:
git stash saves your uncommitted changes (working dir + staged) and reverts the working dir to a clean state, so you can switch branches without losing work.
git stash # stash with default message
git stash push -m "wip auth fix" # stash with a message
git stash list
# stash@{0}: On feature: wip auth fix
git stash pop # apply latest stash and remove it
git stash apply stash@{1} # apply specific stash, keep it
git stash drop stash@{0} # delete a stash
git stash show -p stash@{0} # show what's in the stash
git stash -u # include untracked filesAnswer:
Cherry-pick copies a single commit from one branch onto your current branch.
git switch main
git cherry-pick abc1234 # apply commit abc1234 here
git cherry-pick abc1234 def5678 # multiple commits
git cherry-pick A..B # range (exclusive of A)
git cherry-pick --abortCommon use: hotfix on main needs to be replayed onto a release branch.
Answer:
- Lightweight tag: just a name pointing to a commit. No metadata.
- Annotated tag: a full Git object with author, date, message, optional GPG signature. Recommended for releases.
git tag v1.0.0 # lightweight
git tag -a v1.0.0 -m "Release 1.0.0" # annotated
git tag -s v1.0.0 -m "Signed" # signed (GPG)
git tag # list
git show v1.0.0
git push origin v1.0.0 # push one tag
git push origin --tags # push all tags
git tag -d v1.0.0 # delete locally
git push origin :refs/tags/v1.0.0 # delete remotelyAnswer:
Semantic Versioning uses MAJOR.MINOR.PATCH:
- MAJOR: breaking changes
- MINOR: new features, backward compatible
- PATCH: bug fixes, backward compatible
git tag -a v1.4.2 -m "Fix login redirect bug"
git tag -a v1.5.0 -m "Add OAuth2 support"
git tag -a v2.0.0 -m "Drop support for Node 16"Many teams automate tagging from CI based on commit messages (e.g., semantic-release).
Answer:
git remote -v # list remotes
git remote add origin git@github.com:org/repo.git
git remote add upstream git@github.com:upstream/repo.git
git remote set-url origin git@github.com:org/new-repo.git
git remote rename origin github
git remote remove upstreamAnswer:
When you fork someone else's repo on GitHub, your fork is origin. The original repo is conventionally added as upstream.
git clone git@github.com:me/forked-repo.git
cd forked-repo
git remote add upstream git@github.com:original/forked-repo.git
# Stay in sync with the original:
git fetch upstream
git switch main
git merge upstream/main # or: git rebase upstream/main
git push origin mainAnswer:
git push # push current branch to its tracked remote
git push origin feature # push branch to origin
git push -u origin feature # set upstream tracking, then push
git push --tags # push tags
git push --delete origin feature # delete remote branchBy default Git pushes only matching branches; modern Git uses simple mode (push current branch to the same name on remote).
Answer:
Never force-push to a branch that other people are working on. Force-pushing rewrites history; anyone who has pulled the old history will get confused, and resolving the mess is painful.
Force-push only on your own feature branch that nobody else is using. Force-pushing to main or release branches should be blocked by branch protection.
Answer:
--forceunconditionally overwrites the remote branch. If a teammate has pushed something you have not pulled, you will silently destroy their commits.--force-with-leaseonly force-pushes if the remote is in the state you expect (i.e., nobody else pushed since your last fetch).
Always prefer --force-with-lease.
git push --force-with-lease origin featureAnswer:
| Aspect | Gitflow | Trunk-based |
|---|---|---|
| Long-lived branches | main, develop, release/*, hotfix/* |
Mostly main only |
| Feature branches | Long-lived; merged to develop |
Short-lived (hours/days) |
| Releases | From release/* branches |
Tag a commit on main |
| Best for | Slow release cycles, multiple versions | Continuous delivery |
Modern teams favor trunk-based with feature flags for new functionality.
Answer:
- Branch off
mainfor each feature/bugfix. - Keep branches short-lived (a few days max).
- Rebase or merge
maininto your branch frequently to reduce conflicts. - Open a pull/merge request when ready.
- Squash or rebase-merge to keep
mainhistory clean. - Delete the branch after merging.
git switch main
git pull
git switch -c feature/cart-coupon
# ... edit, commit, push, open PR ...Answer:
GitHub/GitLab usually offer three:
| Strategy | Result on main | Best for |
|---|---|---|
| Merge commit | All feature commits + a merge commit | Preserve detailed history |
| Squash & merge | One commit on main summarizing the PR | Clean main, throwaway feature steps |
| Rebase & merge | Feature commits replayed linearly (no merge) | Clean main, keep individual commits |
Most teams choose squash or rebase for main and merge commits for long-lived release branches.
Answer:
- Keep PRs small (~200-400 lines of diff if possible).
- One logical change per commit.
- Write a good PR description: what, why, how to test.
- Respond to feedback with new commits while review is in progress; squash before merging.
- Test locally before clicking "approve."
- Re-request review after substantive changes.
Answer:
Git hooks are scripts in .git/hooks/ that run at specific points in the workflow. Common hooks: pre-commit, commit-msg, pre-push, post-merge.
# .git/hooks/pre-commit (executable)
#!/usr/bin/env bash
npm test || exit 1chmod +x .git/hooks/pre-commitHooks live outside the repo, so to share them use a tool like pre-commit or husky:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yamlAnswer:
A submodule is a Git repo embedded inside another. The parent repo records a specific commit of the submodule.
git submodule add https://github.com/org/lib vendor/lib
git commit -m "Add lib submodule"
git clone --recursive <repo> # clone with submodules
git submodule update --init --recursive # if you forgot --recursive
git submodule update --remote # update to latest remote commitSubmodules are powerful but tricky — they are commonly avoided in favor of package managers.
Answer:
| Aspect | Submodule | Subtree |
|---|---|---|
| Storage | Reference to external repo | Copy of files merged into history |
| Cloning | Need --recursive |
Just clone, everything is there |
| Updating | git submodule update |
git subtree pull |
| Complexity | Higher; users often forget commands | Lower; transparent |
Answer:
.gitignore lists patterns of files Git should not track. One per line, supports globs.
# Dependencies
node_modules/
vendor/
# Build
dist/
build/
*.o
# Env
.env
.env.local
# IDE
.idea/
.vscode/
# OS
.DS_Store
Thumbs.db
# Logs
*.log
To stop tracking a file that is already committed, you also need git rm --cached:
echo ".env" >> .gitignore
git rm --cached .env
git commit -m "stop tracking .env"Answer:
.gitattributes controls per-path Git behavior: line endings, diff/merge drivers, export behavior.
* text=auto eol=lf
*.png binary
*.lock linguist-generated=true
package-lock.json merge=ours
The first line forces LF line endings, which prevents the classic Windows/Mac/Linux line-ending conflicts.
Answer:
Git LFS (Large File Storage) replaces large files in your repo with small text pointers. The actual file content is stored on a separate LFS server.
git lfs install
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
git add design.psd
git commit -m "add design"
git pushUse LFS for large binary assets that change rarely. Without LFS, big binaries bloat the history forever.
Answer:
Signed commits use GPG or SSH keys to cryptographically prove who made the commit. Otherwise the author field is just a string.
# GPG
git config --global user.signingkey <KEY_ID>
git config --global commit.gpgsign true
git commit -m "signed commit"
# SSH (Git 2.34+)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign trueGitHub shows a green "Verified" badge for signed commits.
Answer:
A worktree lets you check out multiple branches at once into different directories without re-cloning the repo.
git worktree add ../repo-hotfix hotfix/login-bug
cd ../repo-hotfix
# work, commit, push
cd -
git worktree list
git worktree remove ../repo-hotfixUseful when you want to keep your main work intact and quickly fix something on another branch.
Answer:
git bisect does a binary search through the commit history to find the commit that introduced a bug.
git bisect start
git bisect bad # current HEAD is broken
git bisect good v1.0.0 # this older tag was fine
# Git checks out a commit in between; you test
git bisect bad # or 'good' depending on test result
# repeat until git names the culprit
git bisect resetEven better, automate it:
git bisect start HEAD v1.0.0
git bisect run npm testAnswer:
git blame shows who last touched each line of a file.
git blame src/auth.js
git blame -L 100,150 src/auth.js # only lines 100-150git log -L shows the full history of a specific function or line range.
git log -L :functionName:src/auth.js
git log -L 100,150:src/auth.jsAnswer:
git log --oneline --graph --decorate --all
git log --since="2 weeks ago"
git log --author="Alice"
git log --grep="bug" # search commit messages
git log -S"functionName" # commits adding/removing the string
git log --follow src/old-name.js # track renames
git log --stat # file change summary
git log -p # full patches
git log -- src/ # only commits touching src/Answer:
git config --global user.name "Alice"
git config --global user.email "alice@example.com"
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global rebase.autoStash true
git config --global core.editor "code --wait"
git config --global merge.conflictStyle zdiff3
git config --global push.default current
git config --global push.autoSetupRemote true
git config --global fetch.prune true
git config --global help.autocorrect 20Show your current config:
git config --list --show-originAnswer:
git config --global alias.s "status -s"
git config --global alias.co "checkout"
git config --global alias.br "branch"
git config --global alias.cm "commit -m"
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.unstage "restore --staged"
git config --global alias.amend "commit --amend --no-edit"Now git lg gives you a pretty graph; git s is fast status.
Answer:
git clone --depth=1 <url> # shallow clone, only latest commit
git clone --filter=blob:none <url> # partial clone, fetch blobs on demand
git sparse-checkout init --cone # check out only some directories
git sparse-checkout set src/ docs/
# Maintenance
git maintenance start # background tasks: gc, pack, prefetch
git gc --aggressive --prune=now # one-shot deep cleanupAnswer:
git gc (garbage collect) removes unreachable objects and packs loose objects into pack files for speed and space.
git gc # routine
git gc --aggressive --prune=now # one-time aggressive cleanup
git fsck # integrity check
git count-objects -v # repo statisticsModern Git can run maintenance automatically:
git maintenance startAnswer:
- Slow operations: enable partial/sparse clones (
--filter=blob:none,sparse-checkout). - CI selectivity: only build/test packages that changed.
- Atomic, large commits across many directories.
- Shared tooling: monorepos benefit from tools like Nx, Turborepo, or Bazel.
- Codeowners files for review routing.
Answer:
Move the commits to the right branch.
# Suppose you committed N commits on main that should be on feature
git switch -c feature # create feature from current state
git switch main
git reset --hard origin/main # rewind main to clean state
git switch featureOr move just the last commit:
git switch feature # create or switch to right branch
git cherry-pick main # take latest main commit
git switch main
git reset --hard HEAD~1Answer:
The safe way is git revert — it adds a new commit that undoes the change. History is preserved.
git revert abc1234
git pushThe unsafe way is to rewrite history (only on a branch you control):
git reset --hard HEAD~1
git push --force-with-leaseAnswer:
If the commit has not been pushed, git reset and re-commit. If it has been pushed, you must rewrite history and rotate the secret (the secret should be considered compromised).
# Modern tool: git-filter-repo
pip install git-filter-repo
git filter-repo --path secrets.env --invert-paths
# Or BFG Repo-Cleaner
bfg --delete-files secrets.env
# Then force-push (and notify the team)
git push --force --all
git push --force --tagsAlways rotate the leaked credential — assume it has been seen.
Answer:
For the most recent commit:
git commit --amend --author="Alice <alice@example.com>" --no-editFor a range (interactive rebase, then amend each):
git rebase -i HEAD~5
# mark commits as 'edit'
# for each pause:
git commit --amend --author="Alice <alice@example.com>" --no-edit
git rebase --continueAnswer:
git rebase -i <commit>^
# mark target commit as 'edit'
# when rebase pauses:
git reset HEAD^ # un-commit, keep changes in working dir
git add file1
git commit -m "first part"
git add file2
git commit -m "second part"
git rebase --continueAnswer:
git rebase -i HEAD~3
# change last two from 'pick' to 'squash' (or 'fixup')
# save and edit the combined commit messageOr non-interactively:
git reset --soft HEAD~3
git commit -m "combined message"Answer:
git reflog
# find the last commit of the deleted branch, e.g. abc1234
git switch -c recovered-branch abc1234If the deletion happened on a remote, ask whoever has the branch in their reflog (or check the GitHub events log).
Answer:
You will get a merge conflict. Coordinate with the team to decide the right answer.
git pull origin main
# CONFLICT in src/app.js
# manually resolve the file (remove markers, choose content)
git add src/app.js
git commit
# or, with deliberate decision:
git checkout --ours src/app.js # keep your version
git checkout --theirs src/app.js # take incoming version
git add src/app.js
git commitAnswer:
git fetch
git log origin/main..HEAD # commits on local that aren't on origin
git diff origin/main..HEAD # the diff
git push --dry-run # simulate the pushAnswer:
gh auth login
gh repo clone org/repo
gh pr create --title "Add login" --body "..."
gh pr list
gh pr checkout 42
gh pr view 42 --web
gh pr merge 42 --squash
gh issue list --assignee @me
gh run list # CI runs
gh run watchgh is GitHub's official CLI; it is a huge time saver for everyday workflows.
Answer:
.gitignorefor the language and editors.gitattributesenforcing LF- Branch protection on
main: require PR, require checks, require reviews - Conventional commits or another standard
- Pre-commit hooks (lint, format, tests)
- Signed commits (optional, recommended)
- CI runs on every PR
- Squash or rebase merge enforced
- Stale branches deleted on merge
- Tag-based releases (SemVer)
- Secrets scanning enabled