Reference:
Git moves your work through four places. Almost every command is about moving between two of them.
Place
What it is
Working tree (Disk)
The actual files you edit in VS Code
Staging area (Index)
What you've marked to go into the next commit
Local repository
Your committed history, stored in .git
Remote (origin)
The shared repository on GitHub
git add moves Disk → Staging. git commit moves Staging → Local. git push moves Local → Remote.
In addition, we assume that one git branch is owned and worked by only one person.
Clone the repository:
git clone <repository_url>
cd <repository_name>
This downloads the full history into your local repository and writes the current files onto your disk.
mainNever work directly on main. Always branch — but branch from the latest code, or you'll create unnecessary conflicts later.
git checkout main
git pull
git checkout -b <feature_branch_name>
Branch naming: use a prefix and a short description, e.g. feat/user-login, fix/null-pointer-on-save.
Always look at your own changes first.
git status # which files changed
git diff # unstaged changes (Disk vs Staging)
git diff --staged # staged changes (Staging vs last commit)
git diff HEAD # everything not yet committed
Note: once you run
git add, plaingit diffshows nothing. That's expected — usegit diff --stagedto see what you staged.
git add <file_name>
git commit -m "Short description of what changed"
Stage files by name rather than using git add ., so you don't accidentally commit debug code or unrelated files.
Commit messages: imperative mood, short subject line. Add password reset endpoint, not added stuff.
First push on a new branch:
git push -u origin <feature_branch_name>
The -u links your local branch to the remote one. After that, plain git push and git pull work with no arguments.
main (Rebase)If main moves forward while you're working, bring those changes into your branch before opening a PR.
maingit checkout main
git pull
git checkout <feature_branch_name>
git rebase main
Git temporarily sets your commits aside, brings in the latest main, then replays your commits on top.
A conflict means Git found the same lines changed in two places and needs you to decide. This is normal, not an error.
Run git status to see which files are conflicted.
Open each file. You'll see markers like <<<<<<<, =======, >>>>>>>. Edit the file so it has the code you want, and delete all the markers.
Run git add <file> for each file you fixed.
Run git rebase --continue.
Git may stop again with another conflict. Repeat steps 1–4 until it finishes.
If you get stuck or panic: run git rebase --abort. This puts everything back exactly as it was. Nothing is lost. Then ask for help.
Before continuing, check: no <<<<<<< markers left in any file, and the tests still pass. The merged result is a combination that has never been tested before.
Rebasing rewrites your branch's history, so a normal push will be rejected.
git push --force-with-lease origin <feature_branch_name>
Use --force-with-lease, not -f. It does the same thing, but refuses if the remote has commits you don't have locally — for example, a suggestion a reviewer accepted through the GitHub UI.
Never force-push main, and never force-push a branch you don't own.
On GitHub, open a PR from your feature branch into main and request a review.
Before requesting review, read through your own diff on GitHub. It catches leftover debug code and stray files.
Once approved, merge using the "Squash and merge" option.
This combines every commit on your branch into one clean commit on main. Messy work-in-progress commits stay out of the main history.
The PR title becomes the commit message on
main, so give the PR a clear, descriptive title.
Click "Delete branch" on the merged PR page.
git checkout main
git pull
git fetch --prune
git branch -D <feature_branch_name>
git fetch --prune removes references to branches that were deleted on GitHub.
git checkout main && git pull
git checkout -b feat/my-feature
git status
git diff
git add <file>
git commit -m "message"
git push -u origin feat/my-feature
git checkout main && git pull
git checkout feat/my-feature
git rebase main
git push --force-with-lease origin feat/my-feature
git checkout main && git pull
git fetch --prune
git branch -D feat/my-feature
Never commit directly to main.
One person per branch.
Review your own diff before requesting review.
Never force-push main.
Never commit secrets, .env files, keys, or credentials. If you do, rotate the credential immediately — assume it's compromised.
When in doubt, git rebase --abort and ask.
When this applies: contributing to an open-source project, or any repository where you can't push branches directly.
You make your own copy of the repository on GitHub (a fork), push branches there, and open a PR from your fork into the original project. Maintainers never give you write access — they just merge your PR.
Every Git command is the same as the main workflow. The only difference is that you have two remotes instead of one.
Remote
Points at
Can you push?
origin
your fork
Yes
upstream
the original project
No — read only
Click Fork on the project's GitHub page.
Clone your fork — this sets origin automatically:
git clone <your_fork_url>
cd <repository_name>
upstream:git remote add upstream <original_repo_url>
git remote -v
Confirm the output shows origin = your fork and upstream = the project.
CONTRIBUTING.md. Their rules override this workflow — commit format, sign-off requirements, whether they want rebase or merge.GitHub
├── original-owner/project ← upstream (read only)
└── you/project ← origin (your fork, you can push)
│
└── cloned to your machine
Your fork does not update itself. Always sync from upstream first:
git fetch upstream
git checkout main
git merge upstream/main
git checkout -b fix/some-bug
Branching off a stale
mainis the single most common cause of unnecessary conflicts in open-source PRs.Never commit to your fork's
main. Keep it a clean mirror of upstream so syncing is always a fast-forward.
Identical to Phase 2 of the main workflow — git status, git diff, git add, git commit.
Push to your fork:
git push -u origin fix/some-bug
There are three different "mains" in a fork setup, and only one is the real one:
Ref
What it is
Fresh?
main
your local branch
only as fresh as your last sync
origin/main
your fork's main on GitHub
stale — forks don't auto-update
upstream/main
the project's main
the real thing
So rebase onto upstream/main:
git fetch upstream
git rebase upstream/main
Handle conflicts exactly as in Phase 3, step 3. Then force push to your fork:
git push --force-with-lease origin fix/some-bug
--force-with-leasematters even more here: GitHub's "Allow edits by maintainers" is on by default, so a maintainer may have pushed a commit to your branch.
Push your branch, then open your fork's page on GitHub. A banner appears: "branch had recent pushes — Compare & pull request." Click it.
Check the four dropdowns at the top of the PR form:
base repository: original-owner/project base: main
head repository: you/project compare: fix/some-bug
Left = where it's going (the project). Right = where it's coming from (your fork).
The common mistake is leaving base repository set to your own fork. That opens a PR to yourself, and maintainers never see it.
You don't run the merge — a maintainer does. Once it lands:
git push origin --delete fix/some-bug
git checkout main
git fetch upstream
git merge upstream/main
git push origin main # keep your fork's main in sync
git branch -D fix/some-bug
git worktree)When this applies: you need a second branch checked out while your current work stays exactly as it is — most often an urgent fix while your working tree is dirty.
This is optional. git stash solves the same problem with commands you already know. Worktrees are better when the switch is disruptive: uncommitted work you don't want to risk, different dependencies per branch, or a long-running process you don't want to interrupt.
A normal clone gives you one folder with one branch checked out:
~/code/
└── project/ ← working tree (your files)
├── .git/ ← the database: all history, all branches
├── src/
└── node_modules/
git worktree lets you have multiple folders sharing one .git:
cd ~/code/project
git worktree add ../project-hotfix -b fix/login-crash main
~/code/
├── project/ ← branch: feat/checkout (dirty, mid-work)
│ ├── .git/ ← THE database
│ └── src/
│
└── project-hotfix/ ← branch: fix/login-crash
├── .git ← a FILE, not a folder: points to ../project/.git
└── src/
One database, two checkouts.
Shared across worktrees?
Commits, branches, tags, remotes
Yes — instantly, no fetch needed
Which branch you're on, staging area, files on disk
No — each worktree is independent
So if you commit in one worktree, git log in the other sees it immediately — but your files don't change. That's the point: your feature work stays frozen.
2:00pm — deep in a feature, 40 modified files, nothing committed.
2:15pm — prod breaks:
git fetch
git worktree add ../project-hotfix -b fix/login-crash origin/main
cd ../project-hotfix
cp ../project/.env . # untracked files do NOT come along
npm install
project/ feat/checkout ← untouched, still dirty
project-hotfix/ fix/login-crash ← clean, branched from origin/main
Fix it, then follow the normal workflow — git status, git diff, git add, git commit, git push -u origin fix/login-crash, open the PR.
After it's merged:
cd ../project
git worktree remove ../project-hotfix
git branch -D fix/login-crash
Your feature branch is exactly as you left it — editor tabs, dev server, everything.
To pull the fix into your feature branch afterwards, that's just the normal Phase 3 rebase:
git fetch
git rebase origin/main
Reviewing a PR while running your own code. Two dev servers, two browser windows, side by side.
Branches with incompatible dependencies. Separate node_modules / venv / build cache per folder, so switching is cd instead of a reinstall.
Long test suites. Run them in one worktree, keep coding in the other.
git worktree list # what exists now
git worktree add <path> -b <new> <base> # new branch
git worktree add <path> <existing-branch> # existing branch
git worktree add <path> <tag-or-sha> # detached, for inspection only
git worktree remove <path> # proper cleanup
git worktree prune # after an accidental rm -rf
Run the command from inside the repo, put the path outside it (../name). Nesting a worktree inside the repo makes it show up in git status and confuses build tools.
One branch per worktree. Git refuses to check out the same branch twice — a branch is a single pointer, and two folders moving it would corrupt each other.
Untracked files don't come along. .env, node_modules, venvs, local config all need setting up in the new folder.
Change the port. Two dev servers can't both use 3000.
Use git worktree remove, never rm -rf. Deleting the folder directly leaves stale metadata behind.
Remove the worktree before deleting the branch. Git won't delete a branch that's checked out in a live worktree.