Developer Fundamentals
Git for Beginners: The Complete Guide and Cheatsheet for 2026
Git is not hard. The documentation is hard. Most tutorials are hard. The way it was taught to you in your first job, by someone who also found it hard, was hard. The underlying thing is a tiny, clean idea: a directed graph of snapshots with pointers attached. Once that idea clicks, every command becomes an operation on a picture you can draw on a napkin.
This guide is the napkin. We start with the mental model — what actually lives in that .git folder — then walk through the daily commands, branching strategies, rebase vs merge, conflict resolution, the reflog safety net, and conventional commits as the glue that turns your commits into useful metadata. Every example is something you can paste into a terminal in a throwaway repo and try immediately.
The mental model: graphs, pointers, snapshots
Three nouns. Everything else is a verb that rearranges them.
Commit. A snapshot of every tracked file, plus metadata: author, timestamp, message, and a pointer to one or more parent commits. Each commit is identified by a SHA-1 (or SHA-256 in newer repos) hash of its contents. Identical content produces identical hashes — this is why Git is content-addressable storage.
Branch. A named pointer to a specific commit. The pointer moves automatically when you commit on that branch. Creating a branch creates a new pointer to the current commit; deleting a branch removes the pointer but leaves the commits intact (reachable by SHA until garbage collection).
HEAD. A pointer to the current branch (or sometimes directly to a commit, in "detached HEAD" state). What you see in your working directory is the snapshot at HEAD, plus your uncommitted changes.
Draw it. A ← B ← C ← D is a sequence of four commits. main is a pointer to D. You create a branch feature: now both main and feature point to D. You commit on feature: feature advances to the new E, main stays on D. Every Git operation is a variation on this — move pointers, sometimes create new commits.
When you internalize this, commands stop feeling arbitrary. git reset moves the current branch pointer. git merge creates a commit with two parents. git rebase rewrites commits so their parent pointers change. git cherry-pick copies a commit to the current branch with a new hash. It is all pointer arithmetic on a graph.
The basic loop: status, add, commit, push
The daily Git loop for individual work is four commands:
git status # see what changed
git add <file> # stage changes for the next commit
git commit -m "message"
git push # send commits to the remote
git status is the most-run command in Git. Run it before and after every operation while you are learning. It shows you three categories: untracked files (new files Git has not seen), unstaged changes (modifications to tracked files), and staged changes (what will be in the next commit).
git add . stages every change in the current directory and below. git add -p stages changes interactively, letting you pick hunks of a file. git add -u stages only modifications and deletions, not new files. The pattern that sticks: use git add -p when you want to split unrelated changes into separate commits, and git add . when everything belongs together.
A good commit message is a subject line under 72 characters, a blank line, and a body that answers "why" rather than "what." The diff already shows what changed. The message explains why you changed it. Conventional commit prefixes (feat:, fix:, docs:) make the subject machine-readable. We have a Git Commit Generator that builds conventional-commit-compliant messages from a description of what you did.
Branches and branching strategies
Branches are cheap. Create one for every unit of work and delete it when merged. The command is git switch -c feature-name (modern syntax) or git checkout -b feature-name (works everywhere). Switch back to main with git switch main.
The three strategies you will encounter in 2026:
Trunk-based development. Everyone commits to main many times a day. Feature flags hide incomplete work. Branches live less than 24 hours. This is how Google, Meta, and Amazon engineer — the argument is that integration problems caught early (at every commit) are cheaper than integration problems caught late (at merge time). Requires strong CI and a culture of small commits.
GitHub Flow. Main is always deployable. Every change goes through a pull request from a short-lived feature branch. Merge the PR, delete the branch. This is the default for most small-to-midsize teams because it maps cleanly to GitHub's UI and enables preview deployments per PR. Cloudflare Pages and Vercel assume this flow out of the box.
Git Flow. Long-lived develop and main branches, with release/*, hotfix/*, and feature/* branches in between. Designed for software with scheduled releases (desktop apps, mobile apps). Overkill for most web work and largely fading in 2026, but still appropriate when you cannot roll back easily.
Our bias: GitHub Flow for most web projects, trunk-based for high-maturity teams with strong CI, Git Flow only when you have versioned releases with rollback costs.
Rebase vs merge, decided
Merge creates a commit with two parents:
A ← B ← C ← D ← M (main)
↖ ↗
E ← F (feature, now merged via M)
Rebase replays your commits on top of the target branch:
A ← B ← C ← D ← E' ← F' (feature, rebased onto main)
The tradeoff is history readability vs history accuracy. Merge preserves exactly what happened — you branched here, worked, and integrated there. Rebase produces a linear history that reads cleanly from bottom to top but lies about when things actually happened.
The golden rule: never rebase commits that have been pushed to a shared branch. Rebasing rewrites history by creating new commits with new SHAs; if anyone else has pulled the old SHAs, their repo and yours diverge, and merging the divergence creates duplicate commits in the history.
The pragmatic flow that works for most teams:
- Create a feature branch from main.
- Commit freely. Messy commits are fine at this stage.
- Before opening a PR,
git rebase -i mainto squash fixup commits and clean up the history. - Open the PR.
- If main moves during review,
git rebase mainto keep the feature branch current (force-push to your own branch is fine). - When merging the PR, pick one: merge commit (preserves the branch), squash merge (one commit on main), or rebase merge (linear history, no merge commit). GitHub Flow with squash merge is the 2026 default.
For building the branch-update workflows in your CI and hooks, Git Command Generator scaffolds the specific incantations, and Git Cheat Sheet has every operation indexed by intent.
Conflict resolution without tears
A conflict happens when two branches change the same lines in the same file. Git cannot decide which version wins, so it stops and asks you. The conflicted file contains markers:
function greet(name) {
<<<<<<< HEAD
return `Hello, ${name}!`;
=======
return `Hi, ${name}! Welcome back.`;
>>>>>>> feature-greeting
}
Everything between <<< and === is the current branch's version. Everything between === and >>> is the incoming version. Your job is to pick, combine, or rewrite, then delete all three markers.
After editing:
git add path/to/file # mark as resolved
git commit # finalize merge
# or
git rebase --continue # if in a rebase
git merge --abort # if you want to start over
git rebase --abort # same for rebases
Most IDEs ship a three-way merge UI showing base, left, right side by side with a target pane. VS Code, JetBrains IDEs, and Cursor all have one; it is almost always easier than editing markers directly. For a CLI-only environment, install meld or kdiff3 as a mergetool and run git mergetool.
Preventing conflicts beats resolving them. Keep branches short. Rebase frequently against main. Agree on formatting so the linter-driven whitespace changes do not collide with semantic changes. Run a Diff Checker on the two branches before merging to preview what is about to happen.
Undoing things (without making it worse)
Everyone has typed git reset --hard at the wrong time at least once. The rescue is git reflog, but you want to avoid the panic in the first place. Here are the undo operations ranked by safety:
Unstaged change on a file — discard: git restore path/to/file (destructive, cannot undo unless you have backups).
Staged change — unstage: git restore --staged path/to/file.
Last commit message wrong: git commit --amend -m "new message". Only do this before pushing — amend rewrites the commit SHA.
Last commit included files it should not have: git reset HEAD~1 unstages the last commit while keeping your changes. Restage selectively, recommit.
Completely wrong commit, not yet pushed: git reset --hard HEAD~1 removes the commit and your changes. Terrifying but reversible via reflog for about 90 days.
Already pushed, need to undo: git revert HEAD creates a new commit that inverts the previous one. Safe on shared branches because no history is rewritten.
The rule: revert on shared branches, reset on private branches. Force-push (git push --force-with-lease) is acceptable on your own feature branch; never on main unless you own the repo and have communicated.
Reflog: the safety net you will need
git reflog logs every change to HEAD for the last 90 days (default). Every commit, reset, rebase, checkout, and branch operation leaves an entry. Even if a commit is unreachable from any branch, it lives in the reflog until garbage collection runs.
$ git reflog
a7b3c21 HEAD@{0}: reset: moving to HEAD~2
e4f5d82 HEAD@{1}: commit: fix: typo in README
3c9a1f0 HEAD@{2}: commit: feat: add dark mode
9b8a7e5 HEAD@{3}: checkout: moving from main to feature-dark
To recover commit e4f5d82 that was lost by the reset:
git checkout e4f5d82 # inspect it
git reset --hard e4f5d82 # restore the branch to it
# or
git branch recovered e4f5d82 # create a new branch pointing to it
The reflog has saved careers. Read it before you panic. Before every "I think I lost my work" feeling, run git reflog and look for the SHA you need.
Conventional commits and why they matter
A commit message like "fix stuff" tells the future no useful thing. A conventional commit like fix(auth): refresh token not invalidated on logout tells a machine it is a bug fix in the auth scope, tells a human what the bug was, and feeds directly into an automated changelog.
The structure:
<type>(<scope>): <subject>
<body, optional>
<footer, optional — BREAKING CHANGE: ..., Closes #123>
Standard types: feat (new feature), fix (bug fix), docs, style (formatting), refactor, test, chore, perf, ci, build. feat! or a BREAKING CHANGE: footer marks an API-breaking change.
Why it pays off:
- Automatic semver bumps: feat → minor, fix → patch, breaking → major.
- Automatic changelogs via tools like
release-please,standard-version, orsemantic-release. - Better PR triage: skim the list, know what is in the release without reading diffs.
- Bisect-friendly: smaller, well-labeled commits make
git bisectactually work.
Enforce the format with a commit-msg hook (via commitlint) or a pre-commit-style framework. For teams new to the format, Git Commit Generator turns a description into a conventional-compliant message. For changelog generation, Changelog Generator compiles commits into Keep-a-Changelog format.
The staging area as a feature, not an obstacle
The staging area (the "index") is a feature that feels like an obstacle until it does not. It sits between your working directory and the commit, letting you choose exactly what goes into the next commit without committing everything at once.
The two workflows that make staging click:
Splitting unrelated changes. You fixed a bug, and while you were in the file you also improved a variable name and added a TODO. Those are three separate commits. git add -p walks the diff hunk by hunk and asks "stage this?" — so you can commit the bug fix, then the refactor, then the TODO, each with a focused message.
Staging a partial file. You refactored a 200-line file but the test only needs the top 20 lines. git add -p lets you stage exactly those 20 lines, commit the test-related change, and keep the rest of the refactor for a separate commit.
The discipline pays off later — a history of focused commits is much more useful to bisect, to revert, and to review than one sprawling commit per day.
Working with remotes: fetch, pull, push
A remote is a version of the repository hosted elsewhere. The default name is origin.
git fetch downloads commits from the remote without changing your working directory. Run it to see if anything new has happened upstream without disturbing your work.
git pull is git fetch followed by git merge (or git rebase, if configured). It updates your branch with remote changes. Configure rebase-on-pull for cleaner history:
git config --global pull.rebase true
git push sends your commits to the remote. git push -u origin feature-branch sets the upstream for a new branch so future push/pull work without arguments. git push --force-with-lease is the safer force push — it refuses to overwrite remote changes you have not seen.
If you work across multiple remotes (personal fork + upstream repo), add the upstream:
git remote add upstream https://github.com/original/repo.git
git fetch upstream
git rebase upstream/main # sync your fork with upstream
Team workflows in 2026
A 2026-current team workflow:
- Create a feature branch off main:
git switch -c feat/user-profile. - Commit conventionally, pushing often to trigger CI previews.
- Open a draft PR early for visibility.
- When ready for review, rebase against main, clean up commits, mark ready for review.
- Reviewer comments addressed with fixup commits (
git commit --fixup=HEAD) for clean diffing. - Once approved, autosquash-rebase (
git rebase -i --autosquash main) to collapse fixups. - Merge via squash-merge on GitHub. The squash commit message is the PR title + body. Branch auto-deleted.
- CI pulls
main, deploys to production via Cloudflare Pages or Vercel.
The auto-deploy step depends on your infrastructure. For the Pages-specific setup, see our Cloudflare Pages Deployment Guide.
Cheatsheet: 30 commands you will actually use
| Command | What it does |
|---|---|
git init | initialize a repo |
git clone <url> | copy a remote repo locally |
git status | show working tree state |
git add <file> | stage changes |
git add -p | stage hunks interactively |
git commit -m "msg" | commit staged changes |
git commit --amend | modify the last commit |
git log --oneline --graph | compact graphical log |
git diff | unstaged changes vs HEAD |
git diff --staged | staged vs HEAD |
git branch | list branches |
git switch -c name | create and switch branch |
git switch name | switch to existing branch |
git merge branch | merge branch into current |
git rebase branch | replay current on top of branch |
git rebase -i HEAD~5 | interactively edit last 5 commits |
git cherry-pick <sha> | copy a commit to current branch |
git stash | save uncommitted work |
git stash pop | restore stashed work |
git reset HEAD~1 | undo last commit, keep changes |
git reset --hard HEAD~1 | undo last commit, discard changes |
git revert <sha> | create inverse commit |
git restore file | discard unstaged changes |
git restore --staged file | unstage |
git reflog | all HEAD movements |
git fetch | download remote refs |
git pull | fetch + merge |
git push | send commits to remote |
git push --force-with-lease | safe force push |
git bisect start | binary-search for a bad commit |
Dev tools that smooth out Git workflows
The browser-based utilities that fit into a Git-driven dev loop:
- Git Commit Generator — conventional commit messages from a description
- Git Cheat Sheet — indexed commands by intent
- Git Command Generator — build specific invocations from a plain description
- .gitignore Generator — language- and framework-specific ignore files
- Changelog Generator — convert commits into Keep-a-Changelog format
- GitHub README Generator — structured README scaffolding
- GitHub Actions Generator — scaffold CI workflows
- Diff Checker — compare arbitrary text before committing
- Markdown Editor — preview PR descriptions and READMEs
- YAML Validator — validate workflow files before pushing
- JSON Validator — validate package.json and lockfiles
- UUID Generator — for fixture data and branch naming
- Case Converter — normalize branch names (kebab-case)
- Character Counter — verify commit subject under 72 chars
All run in the browser — when you are generating ignore files or commit messages from internal project names, nothing leaves the machine.
Related reading
For shipping the commits to a production site, see Cloudflare Pages Deployment Guide. For the CI/CD patterns that run on top of conventional commits, see Indexing API + IndexNow for Static Sites.
FAQ
What is the right mental model for Git?
Directed graph of commits with named pointers (branches). HEAD is the current pointer. Every command manipulates the graph — commits add nodes, branches move pointers, merges add multi-parent nodes, rebases rewrite parent edges. Once you can draw the graph, the commands stop feeling arbitrary.
Should I use rebase or merge?
Both, at different times. Rebase your own feature branch against main to keep it current and clean. Merge feature branches into main via squash-merge on the PR. Never rebase commits others have pulled.
How do I resolve a merge conflict?
Open the conflicted file. Choose the final content between the <<< and >>> markers. Delete the markers. Save. git add the file. git commit (or git rebase --continue). Use your IDE's three-way merge UI when the file is nontrivial.
How do I undo a commit I already pushed?
git revert <sha> creates a new commit that inverts the previous one. Safe on shared branches because no history is rewritten. Do not use reset --hard + force push on shared branches unless you have coordinated with everyone who has pulled.
What is the reflog?
A log of every change to HEAD in the last 90 days. Even unreachable commits remain in the reflog until garbage collection. If you lose work to a bad reset, check reflog first — the SHA you need is probably still there.
Why should I use conventional commits?
Machine-readable subjects enable automated changelogs, semver bumps, and PR filtering. They are also a forcing function: writing feat(auth): ... makes you think about whether the change is actually a feature or actually a fix, which improves the commit.
What is trunk-based development and should I do it?
Everyone commits to main many times a day, with feature flags hiding incomplete work. Requires strong CI, small commits, and team discipline. Works beautifully at Google-scale. Overkill for most small teams, where GitHub Flow is a better fit.
Closing thought
Git is a graph. Commits are nodes. Branches are pointers. Every command is pointer arithmetic. Internalize the picture and you stop memorizing commands — you compose them from the operation you want. The commands will still surprise you occasionally (they surprise everyone), but you will know which mental-model tools to reach for instead of Googling "how to undo git thing."