webdev.complete
🌿 Git & GitHub
🛠️Dev Toolbelt
Lesson 49 of 117
30 min

Git Basics

init, add, commit, log, branch, merge — the daily 80%.

Git looks like 200 commands. It's actually 4 ideas and a dozen verbs. Once the mental model clicks, every command stops being magic and starts being obvious. Let's build that model.

The mental model in two sentences

A commit is a snapshot of every tracked file at a moment in time. A branch is a sticky-note pointing at one of those snapshots.

That's it. Every operation in git is some variation on "take a snapshot," "move a sticky note," or "compare two snapshots." If a command ever confuses you, ask which of those three it's doing.

Commits are not diffs
A common misconception is that commits store the changes between versions. They don't. Each commit stores a complete snapshot of every file. Git is just clever about not duplicating identical content on disk.

Starting a repo

bash
mkdir my-project
cd my-project
git init
# Initialized empty Git repository in /Users/ada/my-project/.git/

# git stores everything in the hidden .git directory
ls -la
# .  ..  .git

That hidden .gitfolder is the whole repository. Delete it and you're back to a plain folder. Copy it and you have a full history clone.

The three areas: working dir, staging, repo

Every file lives in one of three places at any moment:

  • Working directory: the files you edit in your editor.
  • Staging area (the index): a draft of the next commit. Files you've marked with git add.
  • Repository (.git): the permanent record of past commits.

git add moves changes from working dir to staging. git commit moves staging to repo. git status shows you what's where.

status, add, commit

bash
echo "# my project" > README.md
git status
# On branch main
#
# No commits yet
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#         README.md
#
# nothing added to commit but untracked files present

git add README.md
git status
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#         new file:   README.md

git commit -m "Initial commit"
# [main (root-commit) a1b2c3d] Initial commit
#  1 file changed, 1 insertion(+)
#  create mode 100644 README.md

Every commit has a SHA: a 40-character hash like a1b2c3d4.... The first 7 characters are usually unique enough to refer to it. SHAs never change. They are the commit's permanent ID.

Stage carefully
git add .stages everything. That's often what you want, but it's also how secrets and big binaries sneak in. Get in the habit of git status before every commit, and prefergit add <files> when you can.

Seeing history: log and diff

bash
git log
# commit b4f8a91c7e2... (HEAD -> main)
# Author: Ada <ada@example.com>
# Date:   Fri May 24 10:14:02 2024
#
#     Add hero section
#
# commit a1b2c3d4e5f...
# Author: Ada <ada@example.com>
# Date:   Fri May 24 09:58:17 2024
#
#     Initial commit

# more compact view (the alias every dev sets up)
git log --oneline --graph --decorate
# * b4f8a91 (HEAD -> main) Add hero section
# * a1b2c3d Initial commit

git diff shows changes. It has three useful flavors:

bash
git diff                  # working dir vs staging (unstaged changes)
git diff --staged         # staging vs last commit (what you'd commit)
git diff main feature     # all changes between two branches
git diff a1b2c3d b4f8a91  # between any two commits
bash
git diff
# diff --git a/README.md b/README.md
# index e69de29..d1f0e8c 100644
# --- a/README.md
# +++ b/README.md
# @@ -1 +1,3 @@
# -# my project
# +# My Project
# +
# +A small experiment.

Branches: cheap, instant, everywhere

A branch is just a movable pointer to a commit. Creating one is instant. There's no copying, no fork in the road, just a new sticky note. The default branch is usually called main (older repos: master).

bash
git branch                         # list branches
# * main

git branch feature/login           # create one
git switch feature/login           # move HEAD to it (modern syntax)
# Switched to branch 'feature/login'

# the old way that still works
git checkout feature/login

# create and switch in one step
git switch -c feature/login

HEADis git's name for "the commit I'm currently on." When you commit, HEAD moves forward and drags whichever branch was pointing at it along too.

Merging

Once your branch has the changes you want, merge them back into main. Git tries to be smart and most merges just work.

bash
git switch main
git merge feature/login
# Updating a1b2c3d..b4f8a91
# Fast-forward
#  src/auth.ts | 42 ++++++++++++++++++++++++++++++++++++++++++
#  1 file changed, 42 insertions(+)

A fast-forward merge happens when main hasn't moved since you branched. Git just slides the sticky note forward. If main has its own new commits, you get a real merge commit that joins the two histories.

bash
# when both branches have moved, you'll see something like:
# Merge made by the 'ort' strategy.
#  src/auth.ts | 12 ++++++++++++
#  src/home.ts |  4 +---
#  2 files changed, 13 insertions(+), 3 deletions(-)
Merge conflicts are normal
When both branches change the same lines, git stops and asks you to resolve. Files will have <<<<<<< markers showing each side. Edit, save, git add, then git committo complete the merge. Don't panic, this is routine.

Working with a remote: push, pull, clone

A remote is another copy of your repo, usually on GitHub or similar. origin is the conventional name for the one you cloned from.

bash
# start from someone else's repo
git clone https://github.com/vercel/next.js.git
cd next.js

# see your remotes
git remote -v
# origin  https://github.com/vercel/next.js.git (fetch)
# origin  https://github.com/vercel/next.js.git (push)

# get the latest commits from origin
git pull
# Already up to date.

# upload your local commits to origin
git push
# To github.com:ada/my-project.git
#    a1b2c3d..b4f8a91  main -> main

# pushing a new branch for the first time
git push -u origin feature/login
# -u sets it as the upstream so future 'git push' is enough

pull is really two steps: fetch (download commits) followed by merge(apply them). You'll learn to use fetch alone when you want to see what changed before merging.

A daily workflow

This loop happens dozens of times a day:

bash
git switch main
git pull                          # sync with the world

git switch -c feature/cool-thing  # branch off
# ... edit files ...

git status                        # what changed?
git diff                          # what exactly changed?
git add src/cool.ts               # stage
git commit -m "Add cool thing"    # snapshot

git push -u origin feature/cool-thing
# ... open a Pull Request on GitHub ...

Quick quiz

Quiz1 / 3

What does a git commit actually store?

Recap

  • A commit is a snapshot. A branch is a pointer. HEADis "where I am."
  • Three areas: working dir → (git add) → staging → (git commit) → repo.
  • git status and git diffanswer "what changed?" before you commit.
  • git switch -c name creates and moves to a new branch. git merge name brings it back.
  • git push and git pull sync your local history with a remote like GitHub.