← Course page Git for Beginners
0/6 modules

Module 1 — What Git is and why it exists

Git data model and four file states. ~5 minutes.

Snapshots, not deltas

When you make a commit, Git stores a full snapshot of the entire project at that moment. Not a diff from the previous commit — every file in full (with deduplication inside the store, so it actually takes little space).

History is a tape of snapshots:

commit C  →  index.html  app.js  README.md   (full snapshot)
   ↑
commit B  →  index.html  app.js               (full snapshot, app.js was different)
   ↑
commit A  →  index.html                       (full snapshot, app.js did not exist yet)

Analogy: a photo album, not an edit diary. Rolling back to any version is simply “pull that snapshot out.”

Four file states

The same file in Git can be in one of four states. Understanding the transitions between them is the key to everything else.

1. Untracked — Git does not know about the file

The file sits in the project folder but has never been added to Git. git status will show it in the red “Untracked files” section.

Example: you created notes.txt and have not yet run git add.

2. Modified — Git knows the file and sees changes

The file was committed at some point; you changed it, but the changes are not in the stage yet. git status will show it under “Changes not staged for commit”.

Example: you edited app.py, which was in the last commit.

3. Staged — in the index, ready to commit

You ran git add: the changes are parked in the stage (also called the index). This is the draft of the next commit. In git status — the “Changes to be committed” section.

Example: git add app.py succeeded; git commit has not run yet.

4. Committed — recorded in history

After git commit the snapshot goes into the repo history. From that moment the state is locked in and you can retrieve it whenever you want.

Lifecycle of a single change:

Untracked  →  (git add)  →  Staged  →  (git commit)  →  Committed
                                            ↑
              edited the file  →  Modified  →  (git add)  →  ...

Local and remote

All of Git history lives in a hidden .git/ folder inside the project — that is the local repository. No internet is needed for basic work: commits, branches, history — all local.

A remote repository is a copy of your repo on a server (GitHub, GitLab, Bitbucket, or your own). You talk to it with git push (send local commits) and git pull (bring in other commits). Details in Module 5.

Check yourself

You just created a file todo.md and saved it. You have not run git add yet. What state is it in?
Untracked
Modified
Staged
Committed
The file app.py was in the last commit. You edited it, saved, but did not run git add. What state is the file in?
Untracked
Modified
Staged
Committed
You ran git add app.py. It succeeded; you have not run git commit yet. What state is the file in now?
Untracked
Modified
Staged
Committed
You now understand the snapshot model and can tell the four file states apart. Next — how to create a repository and start working with it.

Module 2 — Setup and creating a repository

First Git configuration, git init, .gitignore. ~7 minutes.

First setup (once after installing Git)

Git needs to know who is making commits. These are global settings — set them once, and they apply to every project on this machine:

git config --global user.name "Your Name"
The commit author name. Visible to anyone who looks at the history.
git config --global user.email "you@example.com"
Preferably the same email as on GitHub/GitLab — then commits will automatically link to your account.

Check what was recorded:

git config --global --list
Shows all global settings.

Creating a repository: init vs clone

Two ways to start using Git in a project:

git init — a new repository from an existing folder

Go into the project folder (it may already have your code, or nothing yet) and run:

git init
Creates a hidden .git/ folder — that is where the entire history will live. Git does not touch the project files.

git clone — download an existing repository

git clone <url>
Creates a folder named after the repo next to you and pulls the full history into it. After that you have a local copy with all branches. Use this when you are working with a project that already exists on GitHub/GitLab.

.gitignore — what Git should ignore

Not everything belongs in the repo: build artifacts, caches, secrets, virtual environments. The list of what Git should skip lives in a text file .gitignore at the project root. The file itself is committed.

# dependencies / caches
node_modules/
__pycache__/
*.pyc

# build
dist/
build/

# secrets — never commit
.env
.env.local

# logs
*.log

# IDE
.vscode/
.idea/

One pattern per line. Glob is supported: *.log, build/**. Lines with # are comments. Useful tip: add .gitignore at the very beginning, before the first commit — otherwise junk can slip into history.

Open Source Control: Ctrl+Shift+G (or the branching icon in the left sidebar). If there is no repo yet — you will see an Initialize Repository button; it simply runs git init. If a repo exists — you will see a panel with current changes: to the right of each file, + (stage) and (discard).

Try it in the trainer

This terminal supports basic git commands. Two files are already sitting in it: README.md and index.html — but without git so far. Do this in order:

  1. git init — create the repo
  2. git status — see what Git sees (there are untracked files already there)
  3. git add . — add everything to the stage
  4. git status — notice how the files moved from “Untracked” to “Changes to be committed”
  5. git commit -m "init" — lock in the first snapshot in history
  6. git log --oneline — confirm the commit appeared
You can now configure Git and create a new repository — via the CLI and via VSCode.

Module 3 — The basic working cycle: stage → commit

The main everyday loop. ~8 minutes.

Three commands you will type constantly

git status
What is in which state right now: staged, modified, untracked. Run it constantly — between almost every pair of commands. This is your main compass.
git add <file>
Add a specific file to the stage. You can pass several, space-separated: git add a.py b.py.
git add .
Add everything changed and new in the current folder (and subfolders) to the stage. Convenient, but treacherous: easy to commit extras. Always run git status first.
git commit -m "Add login button"
Record everything in the stage as a new snapshot in history. The -m flag is a short message. Without it Git opens an editor.
In the Source Control panel: the + next to a file → stage that file. The + in the “Changes” header → stage all. The message field at the top → commit text. The ✓ Commit button (or Ctrl+Enter) → commit. After a commit the change list clears.

Good commit messages: the 50/72 rule

A commit message is a note to your future self and to colleagues. Six months later, when something breaks, you will search “when did we change this and why” via git log. Message quality == search quality.

Rules

  • Title ≤ 50 characters. Fits on one line in any UI.
  • Imperative mood: “Add user auth”, “Fix race in pool”, “Update README”. Not “Added”, not “Fixing”.
  • Meaning, not mechanics: “Fix race in connection pool”, not “Edit pool.go”.
  • If you need detail — a blank line after the title, then a body wrapped at 72 characters per line (hence “50/72”).

Good / bad

✓ Good:

Add login button to header

Hooks up to existing /auth/login endpoint.
Closes ticket #PAY-142.

✗ Bad:

fix
update file
.
WIP
asdfasdf

Bad messages turn git log into noise. Better spend 10 seconds on the wording — it saves hours of future searching.

Sort the files by state

Drag each file into the correct state. Each one is labeled with what was just done to it.

Available files
notes.txt (new, no git add yet)
app.py (just ran git add app.py)
README.md (committed yesterday, unchanged)
.env.local (just created)
config.json (changed and git add ran)
main.go (committed in the last commit)
Untracked
Staged
Committed
Drag files from the pool into the right zone. Correct ones turn green, wrong ones turn red. Mixed them up — drag to another zone or back to the pool.
You can now run the add → commit cycle and write messages that will still make sense in six months.

Module 4 — Branches

Parallel lines of work. ~10 minutes.

What a branch is

A branch in Git is a movable pointer to a commit. That is all. Not a copy of files, not a separate folder — just a text reference to one commit hash.

When you make a new commit, the branch pointer automatically moves to that new commit. The current branch is stored in a special pointer, HEAD.

Analogy: a branch = a parallel universe of the code. You can start a new line of work, experiment, then either merge it back into main or delete it if it did not work out.

Commands

git branch
List of local branches. An asterisk before the name is the current one.
git branch feature-x
Create a feature-x branch from the current commit. It does not switch to it — it just creates the pointer.
git switch feature-x
Switch to the feature-x branch. The modern synonym of the old git checkout feature-x.
git switch -c feature-x
Create a branch and switch to it immediately (one command). The old equivalent is git checkout -b feature-x.
git merge feature-x
Merge feature-x into the current branch. Typical scenario: switch to main, then merge your feature into it.
Bottom left, in the status bar — the current branch name. Click it to open a dropdown: a list of existing branches (picking one = switch), and a Create new branch button (= switch -c). Very handy for daily work.

Merge vs Rebase — conceptually

Merge

Creates a merge commit with two parents: your last commit and the last commit of the branch being merged. History is preserved as-is — you can see what ran in parallel. Safe (does not rewrite the past), but the graph gets bushy.

Rebase

Rewrites the branch commits as if they originally grew on top of the target branch. History comes out linear and clean. But the commits are recreated with new hashes — do not rebase something already pushed and used by others.

This course works only with merge — it is simpler and safer to start with.

Play with a branch

Each button below = one git command. The graph redraws after every step. The recommended scenario is top to bottom, so you get a merge commit:

  1. git commit (on main — it is currently active) — one more commit on main
  2. git branch feature — create the feature branch (a pointer)
  3. git switch feature — switch to it
  4. git commit — a commit now on feature
  5. git switch main — back to main
  6. git commit — a commit on main (now main and feature have diverged)
  7. git merge feature — merge
If you followed the scenario in order (commit → branch → switch feature → commit → switch main → commit → merge feature) — how many merge commits are in the graph?
0
1
2
3
You can now create branches, switch, merge, and you understand how merge differs from rebase.

Module 5 — Working with a remote repository

push, pull, fetch, and why origin/main exists. ~7 minutes.

Linking to a remote repo

First — once — tell Git where the remote copy lives:

git remote add origin <url>
Bind the local repo to a remote. origin is the traditional name for the primary remote. The URL is what GitHub/GitLab shows on the repo page (HTTPS or SSH).

Send local commits — push

git push -u origin main
First push. The -u flag (or --set-upstream) remembers the link “local main ↔ origin/main”, and after that you can simply:
git push
With no arguments — sends the current branch to where it is tracking. The most common command.

Bring in other commits — pull and fetch

git pull
Download other commits AND immediately merge them into your branch. pull = fetch + merge (or + rebase, depending on settings).
git fetch
Only download the updated origin branches into the local cache — no merge. After fetch your code has not changed; you can look at what is new with git log origin/main, then decide what to do.

What origin/main is

This is a tracking branch — a local cache of how the main branch looked on the remote at the last fetch or pull. It can lag behind the real origin (until you fetch).

Useful for comparison: “what is new locally compared with origin/main” — git log origin/main..HEAD. Or the other way: “what origin has that you do not” — git log HEAD..origin/main.

In the top of the Source Control panel — three buttons in the “...” dropdown: Sync (= pull + push), Push, Pull. At the very bottom of the window, in the status bar, the ↓N ↑M indicator: commits behind / ahead of origin. Click it — sync.

Scenarios — what to do?

Scenario 1. Locally 2 new commits, origin has nothing new. What do you do to share with the team?
git push
git pull
git fetch
git merge
Scenario 2. Origin has 3 new commits (a colleague pushed), you have no new local commits. What to do?
git push
git pull
git fetch
do nothing
Scenario 3. Both sides have new commits — you have 1 local, origin has 2 from others. What to do?
git push right away
git pull first, then git push
git fetch
delete the branch and clone again
Scenario 4. You want to see what is new on origin, but not merge anything into your branch. What to do?
git pull
git fetch
git push
git status
You now understand push / pull / fetch and know which command to pick in typical situations.

Module 6 — Conflicts and how to resolve them

When Git cannot merge two branches on its own. ~7 minutes.

Why a conflict happens

A conflict = both sides of a merge changed the same line (or adjacent lines) of the same file. Git can merge many changes automatically — but if two versions edit literally the same place, it will not guess which to keep. Then you have to decide by hand.

It happens on git merge, git rebase, git pull (because pull = fetch + merge), sometimes on cherry-pick and stash pop.

Conflict markers in the file

When Git cannot cope, it leaves special markers in the file:

def hello():
<<<<<<< HEAD
    print("Hello, world!")
=======
    print("Hello, universe!")
>>>>>>> feature
    return 0
  • <<<<<<< HEAD — start of your version (current branch)
  • ======= — separator
  • >>>>>>> feature — end of the incoming version (name of the branch you are merging)

You need to: pick one version, or assemble a hybrid by hand, and must delete all three markers. A file with markers is broken code.

Mini merge editor

Click a hunk — it goes into Result. You can “Accept Both”, then both versions stay (and almost always you need a manual edit after that).

HEAD — your branch (main)
def hello(): print("Hello, world!")
Incoming — feature
def hello(): print("Hello, universe!")
(result is empty — click a hunk above)

VSCode Merge Editor — no hand-editing of markers

When a conflict appears, VSCode highlights the file and offers to open the Merge Editor — three panes: Current / Incoming / Result. Buttons Accept Current, Accept Incoming, Accept Both on each conflicted block. At the bottom — a Complete Merge button. You can also just edit the file in the editor — each block has the same three buttons and a “Compare Changes” button.

After you have resolved the conflict

Markers are gone, the code is assembled. Next:

git add <file>
You mark the file as “conflict resolved”. You can git add . immediately if you are sure.
git commit
Without -m — Git will fill in a default message “Merge branch ...”; you only need to save. After that — the merge is done.

If you decide you do not want to continue the merge — git merge --abort rolls everything back to the state before the merge started.

You can now read conflict markers and resolve merge conflicts — by hand or through the VSCode Merge Editor.

Done 🎉

Course complete. You can now:

  • Explain Git’s model: snapshots, not deltas; four file states
  • Configure Git and initialize a repository (CLI and VSCode)
  • Run the git addgit commit cycle and write good messages
  • Work with branches: git branch, git switch, merge
  • Push and pull with a remote via origin
  • Resolve merge conflicts in VSCode

What’s next

Going deeper — the Pro Git book: a free book at git-scm.com. Especially useful are the chapters on rebase, reflog, and the finer points of merge strategies.