Sign in with Google

How to Use Git: The Commands You Actually Need

One mental model — four places your code can live — makes every Git command obvious.

beginner9 min read
dev-setupgitversion-controldeveloper-toolsbeginners

You can memorize twenty Git commands and still have no idea what any of them do. Git commands do not act on "your project" as one blob — they move changes between four separate places. Until you can name those four, git add and git commit look like two arbitrary steps that both mean "save." So the model comes first here, then the commands that cover almost all real work.
iWhat you need
Git already installed — git --version should print something like git version 2.49.0. A terminal, and a GitHub account if you plan to push anywhere. This is not an install guide; if that command errors, install Git first.
This is the whole trick. Every command below is a move between two of these four locations, and once you see them as separate rooms rather than one filing cabinet, the command names start describing themselves.

The four places

  1. 1Working directory — the real files on disk, the ones your editor has open. You change things here; Git notices but does nothing on its own.
  2. 2Staging area (also called the index) — a holding pen for the exact changes you want in your next commit. Nothing arrives here by accident.
  3. 3Local repository — the permanent history on your machine, in the hidden .git folder. Once a change is committed here, it is genuinely hard to lose.
  4. 4Remote — a copy of the repository on another machine, usually GitHub, nicknamed origin. Nothing you commit exists there until you push.
Now re-read the daily commands as movements. git add copies changes from the working directory into staging. git commit writes whatever is staged into the local repository as one permanent snapshot. git push sends new local commits to the remote; git pull brings the remote's down and updates your files. git status answers "what is in the first two rooms?" — which is why you should run it most often.
The staging area is the part worth loving
Beginners treat staging as a pointless extra step. It is what lets you make five unrelated edits in an afternoon and still ship three clean, separate commits — staging one file, or one hunk with git add -p, at a time.
A repository starts existing on your machine in exactly one of two ways.
macOS
$git init
Run inside a folder, that creates a hidden .git directory and changes nothing else — your files are untouched and, for now, untracked. Git still names the first branch master by default and prints a hint saying so. Fix it once, globally, with git config --global init.defaultBranch main.
macOS
$git clone git@github.com:owner/repo.git
Cloning downloads the full history, creates the folder, and wires up the remote called origin, so pushing and pulling work immediately. Most of the time, cloning is how you start.
Almost every working day is the same five commands in roughly the same order. Read the output instead of skipping past it.

The loop

  1. 1git status — see which files changed and which are staged. Run it before and after everything else; it prevents most mistakes.
  2. 2git add src/login.js — stage one specific file. Or git add . for everything below the current directory, which is convenient and occasionally regrettable.
  3. 3git commit -m "Fix redirect loop on expired session" — snapshot what is staged. Leave off -m and Git opens your editor.
  4. 4git pull — bring down anything teammates pushed since you last looked. Doing this first avoids most rejected-push errors.
  5. 5git push — send new commits to the remote. On a new branch, run git push -u origin branch-name once; plain git push works after that.
A commit message is a note to your future self at the moment you are working out why a line of code exists. "update", "fixes", and "asdf" answer nothing. Write a summary line in the imperative mood, around 50 characters, finishing "this commit will…" — then a blank line and a paragraph about why, since the diff already shows the what.
text
Cache the user's timezone on the session object

We looked the timezone up on every render, adding ~40ms per
request. It never changes mid-session, so resolve it at login.

Fixes #412.

A commit message that earns its keep

A branch is not a copy of your project. It is a pointer to one commit — a few dozen bytes. Creating one is instant no matter how large the repository is, which is why the normal habit is a fresh branch per piece of work.
macOS
$git switch -c fix-login-redirect
That creates the branch and moves you onto it in one step. Commit there as usual, then merge: git switch main, git pull, git merge fix-login-redirect. If nothing else touched the same lines, Git fast-forwards or writes a merge commit and you are done. Then git branch -d fix-login-redirect — dozens of merged branches make git branch useless.
iswitch and restore vs. the older checkout
Older tutorials use git checkout because it used to do both jobs: moving between branches and throwing away file changes. That overloading caused real accidents, so Git split it into git switch for branches and git restore for files. Both are the modern default; checkout still works.
A .gitignore file in the repo root lists patterns Git should pretend not to see. Commit anything a teammate needs to run the project; ignore anything a machine can regenerate, and anything dangerous to publish. A committed API key stays in your history forever, even after a later commit deletes the file.
text
# Secrets — never commit these
.env
.env.local
*.pem

# Dependencies (reinstallable from a lockfile)
node_modules/
venv/
__pycache__/

# Build output
dist/
build/

# Editor and OS noise
.DS_Store
.idea/

.gitignore — a reasonable starting point

!Ignoring does not untrack
.gitignore only applies to files Git is not already tracking. If you committed .env first, Git keeps tracking it — run git rm --cached .env, then commit. And treat the key as leaked: rotate it.
This is where people get hurt, so match the command to the situation. Each of these undoes something at a different one of the four places.

Situation, then command

  1. 1You edited a file and want the last committed version back: git restore src/login.js. Your edits are gone for good — Git never had a copy.
  2. 2You staged something you did not mean to: git restore --staged src/login.js. That leaves your edits alone.
  3. 3Your last commit has a typo, or you forgot a file: stage the file, then git commit --amend. This rewrites the commit, so only do it before pushing.
  4. 4You pushed a commit that was wrong: git revert a1b2c3d. That makes a NEW commit undoing the old one, safe on a shared branch because it rewrites nothing.
  5. 5You want to move the branch pointer backwards: git reset, in one of the three flavors below.
The three resets differ only in how much they clean up behind them. git reset --soft HEAD~1 undoes the commit but leaves its changes staged — for recommitting with a better message or splitting it in two. git reset --mixed HEAD~1, the default with no flag, also unstages the changes, leaving them as edits on disk. git reset --hard HEAD~1 deletes them.
--hard deletes uncommitted work permanently
git reset --hard discards your working directory and staging area with no confirmation and no undo. Anything never committed is gone — no reflog will bring it back. Commit or git stash first, every time.
Git is most useful when you are investigating, not when you are saving. Three commands do nearly all of it.
macOS
$git log --oneline --graph --decorate --all
That prints one line per commit with an ASCII graph of how branches diverged and merged, plus labels for where each branch points. Alongside it, git diff shows unstaged changes, git diff --staged shows what your next commit will contain, and git blame src/login.js names the commit that last touched every line.
A conflict means two commits changed the same lines and Git will not guess which wins. It is not a failure state and it breaks nothing. Git pauses the merge, edits the affected files in place to show both versions, and waits for you.
text
function getTimeout() {
<<<<<<< HEAD
  return 30;
=======
  return 60;
>>>>>>> fix-login-redirect
}

What Git writes into a conflicted file

Read the markers literally. Everything between <<<<<<< HEAD and ======= is the version already on the branch you are merging into. Everything between ======= and >>>>>>> arrives from the branch named at the end. Your job is to leave the file reading the way it should read.

Finishing the merge

  1. 1Run git status. It lists every conflicted file under "Unmerged paths" so you never guess which need attention.
  2. 2Edit each one into the correct final state — sometimes one side, sometimes a combination neither branch had.
  3. 3Delete all three marker lines. A surviving marker means code that will not run, and it is the most common way this goes wrong.
  4. 4Stage the resolved file with git add — that is how you tell Git the conflict is settled.
  5. 5Run git commit. Git has already written a merge message, so you can usually accept it as-is.
  6. 6If you would rather not merge at all, git merge --abort puts everything back as it was.

Symptom, cause, fix

  1. 1"Updates were rejected because the remote contains work that you do not have locally." Someone pushed before you. Run git pull, resolve conflicts, push again.
  2. 2"Please tell me who you are." No identity configured. Run git config --global user.name and git config --global user.email once per machine.
  3. 3You committed to main when you meant to be on a branch, and have not pushed. Run git switch -c my-branch to bring the commit with you, then reset main back to origin/main.
  4. 4node_modules is in every commit. It was tracked before .gitignore existed. Add the pattern, run git rm -r --cached node_modules, then commit.
  5. 5You cannot find a commit you know you made. Run git reflog — it records every position HEAD has held, then branch off that hash.
  6. 6git push asks for a username and password every time. Your remote uses HTTPS; switching to SSH removes the prompt.
The next thing worth fixing is authentication: stop typing a token into every push by setting up SSH keys for GitHub. If the terminal is still the slow part, How to Use the Command Line covers the shortcuts that make Git feel fast, and How to Set Up VS Code shows the Source Control panel. For version control in context, the Becoming a Software Engineer roadmap sequences it alongside the fundamentals it supports.