﻿# Git and GitHub: the mental model, in pictures

> Almost every git confusion is a missing picture, not a missing command. The pictures: snapshots, the three areas, branches, remotes, pull requests and undo – with VS Code as the daily driver and the commands beside every button.

Source: https://laszlonemes.com/blog/git-and-github · Published: 2026-09-20 · Author: László Nemes

September 20, 2026 · 18 min read

`Git` `GitHub` `VS Code` `Fundamentals`

Most people learn git as a list of commands to type in the right order. That works until the first unexpected message, and then nothing makes sense – because the commands were never the hard part. Almost every git confusion I have seen, in teams and in the classroom at ELTE, is a missing *picture*: where a change currently lives, what a branch actually is, what "origin" refers to.

So this post is the pictures: 12 figures, a small example in each, and only as many commands as the pictures need. For the day-to-day I use **VS Code**, so every step shows the button next to the command it runs – you should know both, because the button is faster and the command is what every error message and every answer online will speak.

> **The whole model in one breath:** a commit is a snapshot of your project, a branch is a name pointing at a commit, and GitHub is a place where a copy of those commits lives so other people can reach them. Everything else is moving snapshots between places.

## Git is not GitHub

**Git** is a program on your machine. It records the history of a folder and needs no account and no internet. **GitHub** is a website that hosts git repositories and builds collaboration on top of them. GitLab and Bitbucket are alternatives to GitHub; there is no alternative you need for git.

> **Fig. 1** · Every clone is a complete repository with the full history – there is no “server copy” and “client copy”, only copies that agree to treat one of them as the meeting point.
>
> *Diagram:* Git runs on your laptop and holds the full history offline. GitHub hosts a shared copy of the same repository and adds pull requests, issues, CI and releases on top. A teammate's laptop holds another full copy. Only push and pull cross the network.

This is also why "I only want to download one folder" is awkward with git: a repository isn't a folder tree you pick from, it is a sequence of whole-project snapshots. You take the history or you don't.

## Set up once

Install [git](https://git-scm.com/downloads) and [VS Code](https://code.visualstudio.com/), then tell git who you are and which editor to use. You do this once per machine.

```shell
# who you are: stamped into every commit you make
git config --global user.name  "Your Name"
git config --global user.email "you@example.com"

# new repositories start on "main"
git config --global init.defaultBranch main

# when git needs an editor, open a VS Code tab and wait until I close it
git config --global core.editor "code --wait"

# when my branch and the remote have both moved, pull should merge
git config --global pull.rebase false

# line endings: Windows
git config --global core.autocrlf true
# line endings: macOS / Linux
git config --global core.autocrlf input
```

The `core.editor` line matters more than it looks. Out of the box git opens a terminal editor whenever it wants text from you – a merge message, for example – and that is where many beginners get stuck. With this setting it opens a normal VS Code tab instead: type, save, close the tab, and git carries on. If `code` isn't recognised on macOS, run *Shell Command: Install 'code' command in PATH* from the Command Palette first.

The `pull.rebase false` line answers a question git would otherwise stop and ask you later: when both you and the remote have new commits, a pull should *merge* them. Recent git versions refuse to guess. (Rebasing is the other answer; it is a fine tool, and a bad first one.)

Line endings differ between Windows and everything else, and without the `autocrlf` setting a mixed team sees whole files flagged as changed. In a shared project, also commit a `.gitattributes` file containing `* text=auto`, so the rule travels with the repository instead of depending on everyone's laptop.

### Signing in to GitHub

GitHub doesn't accept your account password from git. The path of least resistance: clone with the **HTTPS** URL and let the sign-in happen in the browser. VS Code does this by itself the first time you clone or publish, and Git for Windows ships with a credential manager that does the same for the terminal. **SSH** keys are the alternative many developers prefer – a one-time set-up, no prompts afterwards – and [GitHub's guide](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) walks through it. Both reach the same repository; pick one and move on.

## A commit is a snapshot

A commit is not "the changes I made". It is the **complete state of the project** at one moment, plus a message, an author, a time, and a pointer to the commit that came before. Its ID is a hash of all that, which is why it looks like `a1f93c2` rather than "version 3".

> **Fig. 2** · Three commits. The arrows point backwards, from child to parent – a commit knows where it came from, never what comes after. Storing full snapshots is cheap because unchanged files are shared, not copied.
>
> *Diagram:* Three commits in a row. Each commit is a snapshot of the whole project and points back at its parent. Files that did not change are not copied; the new snapshot points at the copy git already has.

"Changes" are something git *computes* by comparing two snapshots. That comparison works line by line, which is why git loves plain text and is unhelpful with Word documents or images – for those it can only say "this file is different now".

```diff
$ git diff
--- a/login.ts
+++ b/login.ts
@@ -40,7 +40,7 @@ export function login(form: LoginForm) {
   const user = await findUser(form.email);
-  if (user.password == form.password) {
+  if (await verifyHash(form.password, user.passwordHash)) {
     return createSession(user);
```

In VS Code you rarely read a diff in this form: click a file in the Source Control view and you get the same information side by side, old on the left, new on the right. Read it before every commit. It is the cheapest code review you will ever get.

## The three areas

Between "I edited a file" and "it is in the history" there is a stop that surprises everyone coming from Save buttons: the **staging area**. It is a draft of the next commit. You choose what goes into it, so that one commit can be one logical change even if you touched ten files for three different reasons.

> **Fig. 3** · Where a change lives. Forward is always two steps: stage, then commit. Going back is safe everywhere except Discard Changes, which deletes work that was never stored anywhere.
>
> *Diagram:* A change travels through three areas: the working directory, the staging area and the repository. git add stages it, git commit stores it. In VS Code these are the plus icon and the Commit button. Unstaging and undoing a commit move it back; discarding throws it away.

```shell
git init                      # or VS Code: Initialize Repository
git status                    # what does git see?
git add index.html style.css  # stage exactly these
git commit -m "Add home page"
```

In VS Code all of this is one panel – the Source Control view, `Ctrl+Shift+G`. Once you see that its two groups *are* two of the three areas, the panel stops being a list of files and becomes a map.

> **Fig. 4** · The VS Code Source Control view, as a wireframe. If Staged Changes is empty when you press Commit, VS Code offers to stage everything for you – convenient, but it skips the step where you decide what belongs together.
>
> *Diagram:* A wireframe of the VS Code Source Control view mapped onto git's areas: the Staged Changes group is the staging area, the Changes group is the working directory, the plus icon runs git add, and the Commit button runs git commit.

A word on commit messages, since you will write thousands: say *what* changed and *why* in one line, in the imperative – "Fix redirect after login", not "fixed stuff". The diff already shows how. Future you, reading the history to find when something broke, is the audience.

## Branches are names, not copies

The word "branch" suggests a copy of the project. It isn't. A branch is a **name that points at one commit**. That is the entire data structure. Creating a branch writes one tiny file; it is instant no matter how large the project is, which is why git users create branches for everything.

> **Fig. 5** · Two branches after some work on each. The commits a1f and b72 belong to both histories – nothing was duplicated. HEAD marks the branch you are on; in VS Code that is the name in the bottom-left corner of the Status Bar.
>
> *Diagram:* A commit graph with two branches. main and feature/login are just names pointing at one commit each, and HEAD points at the branch you are on. Creating a branch copies nothing.

```shell
git switch -c feature/login   # create the branch and move HEAD onto it
# ...edit, stage, commit, as often as you like...
git switch main               # go back; your files change to match main
git merge feature/login       # bring the work in
```

In VS Code, click the branch name in the Status Bar: the list that opens lets you switch branches or create a new one. When you switch, git rewrites the files in your working directory to match that branch's snapshot – the files on disk really change, and your editor tabs update with them.

### The one scary message: detached HEAD

HEAD normally points at a branch. If you check out a specific commit instead – to look at an old version, say – HEAD points straight at that commit and git tells you that you are in "detached HEAD" state. It sounds like an accident. It is just a description.

> **Fig. 6** · Looking around in detached HEAD is completely safe. The only risk is committing there and then switching away: no branch name points at those commits, so nothing leads back to them.
>
> *Diagram:* Normally HEAD points at a branch, so new commits move that branch. In a detached HEAD state, HEAD points straight at a commit, and new commits belong to no branch until you create one.

## Merging, and merge conflicts

Merging means "make this branch contain that branch's work too". Depending on what happened in the meantime, git does one of two things – and knowing which one explains every history graph you will ever look at.

> **Fig. 7** · Left: nobody touched main while you worked, so there is nothing to combine and main simply moves forward. Right: both sides have new commits, so git creates a merge commit – the only kind of commit with two parents.
>
> *Diagram:* Two outcomes of git merge. If main did not move, git fast-forwards: the main label slides to the tip of the feature branch. If both sides moved, git creates a merge commit with two parents.

Git combines changes automatically as long as the two sides edited different lines. When both sides changed the *same* lines, there is no correct automatic answer, so git stops and hands the decision to you. That is a **merge conflict**. It is not an error and nothing is broken – it is a question.

> **Fig. 8** · How a conflict arises, and where you answer it. “Current” is the branch you are on, “Incoming” is the branch you are merging in. The result pane starts from the common ancestor and you tick which side to take – or type something better than both.
>
> *Diagram:* A merge conflict happens when two branches change the same line differently. Git stops and asks. In the VS Code three-way merge editor the incoming branch is on the left, the current branch on the right, and you build the result at the bottom, then press Complete Merge.

Underneath, git writes both versions into the file between markers, and resolving a conflict means producing the text you actually want and deleting the markers:

```conflict
<<<<<<< HEAD
title = "Home"
=======
title = "Start"
>>>>>>> feature
```

VS Code lists conflicted files under **Merge Changes**. Open one and you get inline actions above each conflict – *Accept Current Change*, *Accept Incoming Change*, *Accept Both Changes* – or press *Resolve in Merge Editor* for the three-pane view in Fig. 8. When every conflict is settled, *Complete Merge* stages the file; commit, and the merge is done. Two habits keep conflicts small: merge often, and keep branches short-lived.

## Local and remote

A **remote** is another copy of the repository that yours knows the address of. The one you cloned from is named `origin` by convention. Four commands cross the network; everything else in this post happens entirely on your machine.

> **Fig. 9** · Which command moves what, and how far. The important asymmetry: fetch only updates your local repository, so it is always safe. pull goes all the way into your files, because it is a fetch followed by a merge.
>
> *Diagram:* Which command moves data where: git add goes from the working directory to the staging area, git commit to the local repository, git push to the remote. git fetch downloads into the local repository only, git pull is fetch plus merge and reaches your files, git clone copies everything the first time.

```shell
# an existing project
git clone https://github.com/you/project.git

# a new one: create an empty repo on GitHub, then
git remote add origin https://github.com/you/project.git
git push -u origin main       # -u: remember that main follows origin/main
```

In VS Code: *Git: Clone* from the Command Palette for an existing project, or the *Publish to GitHub* button in the Source Control view for a new one – it creates the GitHub repository and pushes in one go.

One more name completes the picture. Your repository keeps a bookmark called `origin/main`: where `main` was on GitHub *the last time you talked to it*. It is not live. Comparing it with your own `main` is how git – and VS Code – can tell you that you are ahead, behind, or both.

> **Fig. 10** · The state after a fetch: you and a teammate both committed on top of b72. A pull would now merge d90 into your main (Fig. 7, right side); a push before that would be rejected, because GitHub's main has a commit yours doesn't.
>
> *Diagram:* After git fetch: your main has one commit GitHub does not have, and origin/main, your last-known picture of GitHub's main, has one commit you do not have. VS Code shows this in the status bar as one incoming and one outgoing commit.

That rejected push is the most common first "error" in team work, and the message says exactly what to do: pull first, then push. Git refuses because accepting would throw away your teammate's commit.

## The GitHub flow

Put the pieces together and you get the workflow most teams use. `main` always works; nobody commits to it directly; every change arrives through a **pull request** – a page on GitHub that says "please merge my branch", shows the diff, and gives teammates and automated checks a place to respond before anything lands.

> **Fig. 11** · One piece of work, from branch to merged. The dashed arrow is the normal case, not a failure: review comments lead to more commits on the same branch, and the pull request updates itself when you push them.
>
> *Diagram:* The GitHub flow in six steps across two lanes: on your laptop you create a branch and make small commits, then push. On GitHub you open a pull request, it gets reviewed and checked, and is merged into main. Back on the laptop you pull main and delete the branch.

```shell
git switch main && git pull        # start from the latest main
git switch -c fix/login-redirect   # one branch per piece of work
# ...edit · stage · commit...
git push -u origin fix/login-redirect
# open the pull request on GitHub, get it reviewed and merged, then
git switch main && git pull
git branch -d fix/login-redirect   # done its job (-D if the PR was squash-merged)
```

The pull request is the most valuable thing GitHub adds to git. It turns "my code" into "our code" at a defined moment, with a record of what was discussed. Keep them small: a reviewer can genuinely read 200 lines, and will only skim 2,000.

## Undoing things

Git almost never loses work that was committed. The right way to undo depends on a single question: **how far did the mistake travel?**

> **Fig. 12** · Four stages, four tools. The two red spots are the ones to respect: discarding unstaged edits destroys work git never stored, and rewriting pushed history destroys work that is no longer only yours.
>
> *Diagram:* How to undo depends on how far the mistake travelled: discard an unstaged edit, unstage a staged file, amend or undo a local commit, and revert a pushed commit with a new commit instead of rewriting shared history.

`git revert` deserves a sentence, because it is the safe default once something is pushed: it doesn't delete the bad commit, it adds a new commit that does the opposite. History stays true, nobody's copy breaks, and the revert itself can be reverted.

The opposite tool is `git push --force`, which overwrites the remote branch with yours. On a branch other people use, it silently deletes their commits. If you must rewrite a branch that is *only yours* – after an amend, say – use `git push --force-with-lease`, which refuses if someone else pushed in the meantime. Never force-push `main`.

And when you need to put half-finished work aside for ten minutes – an urgent fix on another branch – that is `git stash` (VS Code: *Git: Stash*). `git stash pop` brings it back.

## What not to commit

A repository should contain what a human wrote and nothing a machine can regenerate. A `.gitignore` file in the project root lists the patterns git should pretend not to see; GitHub maintains [ready-made templates](https://github.com/github/gitignore) for most languages.

.gitignore

```gitignore
# dependencies: reinstalled from package.json, never committed
node_modules/

# build output: rebuilt from source
dist/
build/

# secrets and local settings
.env
.env.local

# OS and editor noise
.DS_Store
Thumbs.db
*.log
```

- **Dependencies and build output** bloat every clone forever and produce meaningless diffs. Rebuild them from source.
- **Large binaries** – installers, videos, datasets – don't diff and never leave the history. Attach built programs to a [GitHub Release](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) instead: tag the commit (`git tag v1.0.0`, `git push origin v1.0.0`) and upload the files there.
- **Secrets** – API keys, passwords, `.env` files. This is the one that hurts. If a secret was ever pushed, deleting it in a later commit does *not* help: it is still in the history, and every clone has it. Treat it as leaked and **rotate it**.

Note that `.gitignore` only affects files git isn't tracking yet. If something was already committed, remove it from tracking with `git rm --cached <file>` and then ignore it.

## Cheat sheet

Everything above, by intent. The VS Code names are Command Palette commands (`Ctrl+Shift+P`) or buttons in the Source Control view.

| I want to… | VS Code | Command |
| --- | --- | --- |
| start tracking a folder | Initialize Repository | `git init` |
| get an existing project | Git: Clone | `git clone <url>` |
| see what changed | Source Control view · click a file for the diff | `git status` · `git diff` |
| stage a file | + next to the file | `git add <file>` |
| commit | Commit · Ctrl+Enter | `git commit -m "…"` |
| create a branch | Git: Create Branch | `git switch -c <name>` |
| switch branch | branch name in the Status Bar | `git switch <name>` |
| upload my commits | Publish Branch · Sync Changes | `git push` |
| download new commits | Sync Changes | `git pull` |
| merge a branch into mine | Git: Merge Branch | `git merge <name>` |
| park unfinished work | Git: Stash | `git stash` · `git stash pop` |
| browse the history | Source Control Graph | `git log --oneline --graph` |

Older tutorials use `git checkout` for both switching branches and restoring files. It still works; `git switch` and `git restore` are the newer commands that split those two jobs apart, and they are what I'd learn today.

## Takeaways

1. **Git is local, GitHub is a meeting point.** Only clone, fetch, pull and push use the network.
2. **A commit is a snapshot** that points at its parent. Diffs are computed, not stored.
3. **Three areas:** working directory → staging area → repository. The Source Control view is a map of the first two.
4. **A branch is a name for a commit.** Creating one costs nothing, so create one for every piece of work.
5. **A conflict is a question, not an error.** Current is you, Incoming is them, the result is your call.
6. **pull = fetch + merge.** When a push is rejected, pull first.
7. **Undo by how far it travelled.** Once it is pushed, revert – don't rewrite.
8. **Never commit secrets.** If it happened, rotate the secret; deleting the file is not enough.

Git's reputation for being hard comes almost entirely from meeting the commands before the model. With the pictures in your head, the error messages start reading like what they are: git telling you, fairly precisely, which arrow in which figure it couldn't follow.

### Further reading

- [Pro Git](https://git-scm.com/book/en/v2) – the free, official book. Chapters 2 and 3 cover everything here in depth.
- [Source Control in VS Code](https://code.visualstudio.com/docs/sourcecontrol/overview) – the official tour of the features used in this post.
- [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow) – GitHub's own description of the branch-and-pull-request workflow.
- [Gitting Started](https://budavariam.github.io/posts/2026/02/04/gitting-started/) by Mátyás Budavári – a friendly, command-line-first introduction that pairs well with this one.
