🌿

Git Beginner

Track and collaborate on code with Git: commits, branches, merges, remotes, conflicts and team workflows.

37 lessons 111 quiz questions
Lessons & quizzes Certificate

📚 Lessons & quizzes

Each lesson ends with its own short quiz. Answer them as you go — score 90% across all lessons to earn your certificate.

1 What Is Version Control and Why Use It?

A version control system (VCS) records changes to a set of files over time so you can recall any earlier version, see who changed what, and work together without overwriting each other’s work. Without it, teams resort to fragile habits like emailing zip files or keeping folders named project_final_v2_REALLY_final.

  • History — every change is saved, so you can roll back a mistake.
  • Collaboration — many people edit the same project safely.
  • Accountability — each change is attributed to an author and a reason.
  • Branching — you can try ideas in isolation, then merge what works.

Git is by far the most popular VCS today. It powers platforms such as GitHub, GitLab and Bitbucket.

# Check your installed Git version
git --version

2 Centralised vs Distributed Version Control

Older systems like Subversion (SVN) are centralised: there is a single central server holding the history, and clients check out a working copy. If the server is down, you cannot commit.

Git is distributed: every clone is a full copy of the entire repository, including its complete history. This means:

  • You can commit, branch, and view history offline.
  • There is no single point of failure — every clone is a backup.
  • Most operations are fast because they happen locally, not over the network.

A “central” repository (like one on GitHub) is just a convention — technically every clone is equal.

# Cloning copies the FULL history to your machine
git clone https://github.com/example/project.git

3 Starting a Repository: git init and git clone

There are two ways to get a Git repository on your machine.

  • git init turns an existing folder into a brand-new, empty repository. Git creates a hidden .git directory that stores all history and metadata.
  • git clone downloads an existing repository (and its full history) from a remote URL.

Before your first commit you should also tell Git who you are, because every commit records an author name and email.

# Create a new repo in the current folder
git init

# Identify yourself (once, globally)
git config --global user.name "Ada Lovelace"
git config --global user.email "ada@example.com"

4 The Three Areas: Working Directory, Staging, Repository

Git organises your work into three conceptual areas:

  1. Working directory — the actual files you see and edit on disk.
  2. Staging area (also called the index) — a snapshot of what will go into the next commit. You move changes here with git add.
  3. Repository (the .git database) — where committed snapshots live permanently.

The flow is: edit files (working directory) → git add (staging) → git commit (repository). The staging area lets you craft a commit from some of your changes rather than all of them.

# See which files are in which area
git status

5 Recording Changes: git add and git commit

git add stages changes; git commit permanently records the staged snapshot in the repository together with a message. Each commit gets a unique SHA-1 hash identifier and points to its parent commit, forming a history.

A good commit is a small, logical unit of change. You can stage a single file, several files, or everything that changed.

The -m flag supplies the commit message inline. Omitting it opens your text editor instead.

# Stage one file, then everything, then commit
git add README.md
git add .
git commit -m "Add project README and initial files"

6 Viewing History: log, diff and show

Git gives you several lenses on your project’s past:

  • git log lists commits, newest first, with author, date and message. --oneline condenses each to a single line.
  • git diff shows line-by-line changes. With no arguments it shows unstaged changes; --staged shows what is staged.
  • git show displays a single commit: its metadata and the exact changes it introduced.
# Compact history graph
git log --oneline --graph

# What have I changed but not staged yet?
git diff

# Inspect one commit in detail
git show a1b2c3d

7 Branches: Create and Switch

A branch is simply a movable pointer to a commit. The default branch is usually called main (older repos use master). Creating a branch is cheap and instant because Git only writes a tiny pointer.

Branches let you develop a feature in isolation without disturbing main. The modern command git switch changes branches; git switch -c creates and switches in one step. (The older git checkout -b does the same.)

# Create and switch to a new branch
git switch -c feature/login

# List branches and switch back to main
git branch
git switch main

8 Merging: Fast-Forward vs Three-Way

Merging integrates the changes from one branch into another. There are two main cases:

  • Fast-forward — if the target branch has not moved since the feature branch started, Git simply slides the pointer forward. No new commit is created.
  • Three-way merge — if both branches have new commits, Git combines them using their common ancestor and creates a special merge commit with two parents.

You merge into your current branch, so switch to the destination first.

# Bring feature/login into main
git switch main
git merge feature/login

9 Merge Conflicts and How to Resolve Them

A merge conflict happens when two branches change the same lines of the same file in incompatible ways. Git cannot decide automatically, so it pauses and marks the conflict inside the file with markers:

<<<<<<< HEAD
your version
=======
their version
>>>>>>> feature/login

To resolve: edit the file to the desired final result, remove the marker lines, then git add the file to mark it resolved, and git commit to finish the merge.

# After editing the file to fix the conflict:
git add conflicted-file.txt
git commit

10 Rebase vs Merge and the Golden Rule

git rebase moves your branch’s commits so they start from the tip of another branch, rewriting them as new commits. The result is a clean, linear history with no merge commits. git merge instead preserves the exact history and adds a merge commit.

The golden rule of rebase: never rebase commits that you have already pushed and that others may have based work on. Rebasing rewrites history (commits get new hashes), which breaks shared branches. Rebase only your own local, unpushed work.

# Replay current branch on top of the latest main
git switch feature/login
git rebase main

11 Remotes: origin, fetch, pull and push

A remote is a version of your repository hosted elsewhere (e.g. on GitHub). The default remote name is origin. Key commands:

  • git fetch downloads new commits from the remote but does not change your working branch.
  • git pull fetches and then merges (or rebases) those changes into your current branch.
  • git push uploads your local commits to the remote.

So pull = fetch + merge. Use fetch when you want to inspect changes before integrating them.

# Send local commits up, bring remote commits down
git push origin main
git fetch origin
git pull origin main

12 Ignoring Files with .gitignore

Not everything belongs in version control: build artefacts, dependency folders, secrets and OS junk should stay out. A .gitignore file lists patterns of files Git should not track.

  • Each line is a pattern, e.g. node_modules/ or *.log.
  • A leading # marks a comment; a trailing / matches directories.

Important: .gitignore only affects untracked files. A file Git is already tracking keeps being tracked even if you add it to .gitignore — you must untrack it explicitly.

# Create ignore rules, then stop tracking an already-committed file
printf 'node_modules/\n*.log\n.env\n' > .gitignore
git rm --cached secrets.env

13 Tags and Releases

A tag marks a specific commit with a permanent, human-friendly name — typically a release version like v1.0.0. Unlike branches, tags do not move.

  • Lightweight tags are just a name pointing at a commit.
  • Annotated tags (created with -a) store extra metadata: tagger, date and a message. These are preferred for releases.

Tags are not pushed automatically; you push them explicitly. Many platforms turn a pushed tag into a downloadable release.

# Create an annotated release tag and push it
git tag -a v1.0.0 -m "First stable release"
git push origin v1.0.0

14 Undoing Changes: restore, reset and revert

Git offers several ways to undo, and they differ importantly:

  • git restore (or older git checkout -- file) discards uncommitted changes in your working files.
  • git reset moves the branch pointer to an earlier commit. --soft keeps changes staged; --mixed (default) keeps them unstaged; --hard discards them entirely (dangerous).
  • git revert creates a new commit that undoes a previous one, without erasing history. This is the safe choice for shared branches.

Rule of thumb: revert public history, reset only your private, unshared commits.

# Discard local edits to a file
git restore app.js

# Undo a pushed commit safely (new inverse commit)
git revert a1b2c3d

# Move branch back, keeping changes staged
git reset --soft HEAD~1

15 Stashing Work in Progress

Sometimes you need to switch branches but are not ready to commit half-finished work. git stash sets your uncommitted changes aside, leaving a clean working directory. You can reapply them later.

  • git stash saves and removes current changes.
  • git stash list shows saved stashes.
  • git stash pop reapplies the most recent stash and removes it from the list.

Stashes are stored on a stack, so you can keep several at once.

# Shelve current work, switch branch, then restore it
git stash
git switch hotfix
# ... later ...
git stash pop

16 Branching Strategies: GitFlow vs Trunk-Based

Teams adopt conventions for how branches are used:

  • GitFlow uses long-lived branches such as main, develop, plus feature/*, release/* and hotfix/* branches. It is structured but can be heavyweight.
  • Trunk-based development keeps a single main branch (“the trunk”). Developers integrate small changes frequently via short-lived branches, relying heavily on automated testing. It favours continuous delivery.

There is no single right answer — GitFlow suits scheduled releases, while trunk-based suits fast, continuous deployment.

# Trunk-based: short-lived branch, merged quickly
git switch -c quick-fix
# make a small change, then merge back to main
git switch main
git merge quick-fix

17 Pull/Merge Requests and Code Review

On platforms like GitHub and GitLab, you propose changes through a pull request (GitHub) or merge request (GitLab). It bundles a branch’s commits and asks maintainers to review and merge them into the main branch.

Code review is the practice of having teammates examine the proposed changes before they merge. Benefits include catching bugs early, sharing knowledge, and keeping a consistent style. Reviewers can comment, request changes, or approve. Automated checks (CI tests) usually run on the request too.

# Push your feature branch so a pull request can be opened
git switch -c feature/search
git push -u origin feature/search
# then open the pull request on the hosting platform

18 Writing Good Commit Messages

A clear commit message explains why a change was made, not just what. A widely used convention has a short subject line (about 50 characters, imperative mood like “Add” or “Fix”), a blank line, then an optional body giving context.

  • Imperative mood: “Fix login bug”, not “Fixed” or “Fixes”.
  • Keep the subject concise; wrap the body at ~72 columns.
  • Explain reasoning and trade-offs in the body when needed.

Good messages make history searchable and help future readers (including you) understand decisions.

# Subject + body via repeated -m flags
git commit -m "Fix off-by-one in pagination" -m "Last page dropped one item because the count was inclusive."

19 Interactive Rebase and Squashing Commits

Interactive rebase lets you rewrite a series of recent commits before sharing them. Running git rebase -i opens an editor listing each commit with an action keyword you can change.

  • pick — keep the commit as is.
  • reword — keep the change but edit its message.
  • squash — combine the commit into the previous one, merging messages.
  • fixup — like squash but discards the commit’s message.
  • drop — remove the commit entirely.

This is ideal for tidying messy “work in progress” commits into one clean commit. Because it rewrites history, only do it on unpushed work.

# Tidy up the last 3 commits interactively
git rebase -i HEAD~3
# In the editor, change 'pick' to 'squash' or 'fixup' on lines you want merged

20 Cherry-Picking Specific Commits

git cherry-pick applies the change introduced by a specific commit onto your current branch, creating a new commit with the same content but a different hash. It is useful when you want just one fix from another branch without merging everything.

Common scenarios include back-porting a bug fix to a release branch, or grabbing a single commit from a colleague’s branch. You can cherry-pick a range too. If the change does not apply cleanly, Git pauses with a conflict that you resolve just like a merge conflict, then run git cherry-pick --continue.

# Apply one commit from another branch onto the current one
git cherry-pick a1b2c3d

# Apply a range of commits (exclusive..inclusive)
git cherry-pick a1b2c3d^..f4e5d6c

21 The Reflog: Recovering Lost Commits

The reflog records where the tips of your branches and HEAD have pointed over time — even after resets, rebases or deleted branches. It is your safety net when a commit seems lost.

Suppose you ran git reset --hard and discarded work. The commit is not gone immediately; it is just unreferenced. git reflog shows entries like HEAD@{2} that you can return to. Reflog entries are local-only and eventually expire (default 90 days for reachable, 30 for unreachable) before garbage collection removes them.

# Find the commit you lost, then restore it
git reflog
git reset --hard HEAD@{2}

22 Finding Bugs with git bisect

git bisect performs a binary search through your history to find the commit that introduced a bug. You mark one commit as good (working) and one as bad (broken); Git then checks out a commit halfway between them. You test, mark it good or bad, and Git narrows the range — halving the candidates each step.

For a history of 1000 commits, bisect finds the culprit in about 10 tests instead of checking each one. When done, run git bisect reset to return to where you started. You can even automate it with a test script via git bisect run.

# Start a hunt: mark current as bad and an old commit as good
git bisect start
git bisect bad
git bisect good v1.0.0
# test, then mark each checkout: git bisect good OR git bisect bad
git bisect reset

23 Submodules: Repositories Inside Repositories

A submodule embeds one Git repository inside another at a fixed commit. The parent repo does not store the submodule’s files directly — it records the submodule’s URL and the exact commit to check out, kept in a .gitmodules file.

This is useful for sharing a library across projects while keeping its history separate. A key gotcha: cloning the parent does not automatically fetch submodule contents — you must initialise and update them, or clone with --recurse-submodules. Updating a submodule means pointing the parent at a new commit and committing that change.

# Add a submodule, then clone a project including its submodules
git submodule add https://github.com/example/lib.git libs/lib
git clone --recurse-submodules https://github.com/example/app.git

24 Working Trees: Multiple Checkouts at Once

git worktree lets a single repository have several working directories checked out at the same time, each on a different branch, all sharing one .git object store. This avoids re-cloning just to look at another branch.

A common use: you are deep into a feature when an urgent hotfix arrives. Instead of stashing and switching, you add a new worktree for the hotfix branch in a separate folder, fix it there, then remove the worktree. Each worktree can only have a given branch checked out once across the repository.

# Add a second working directory on a new branch
git worktree add ../hotfix-dir -b hotfix

# List and remove worktrees
git worktree list
git worktree remove ../hotfix-dir

25 Signing Commits with GPG or SSH

By default, a commit’s author field is just text and can be forged. Signing a commit attaches a cryptographic signature proving it really came from you. Git supports GPG keys and, more recently, SSH keys for signing.

Once configured, you sign a commit with the -S flag, or enable automatic signing globally. Hosting platforms display a “Verified” badge for valid signatures. Verifying a commit checks the signature against the signer’s public key. This matters for supply-chain security where you must trust who authored a change.

# Configure SSH signing, then sign a commit
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git commit -S -m "Add verified change"

26 The pre-commit Hooks Framework

Git can run scripts automatically at certain points via hooks stored in .git/hooks. Managing these by hand across a team is awkward, so the popular pre-commit framework standardises them. You declare the checks you want in a .pre-commit-config.yaml file, and the tool installs a Git hook that runs them before each commit.

Typical checks include linters, formatters, trailing-whitespace fixers and secret scanners. If any check fails, the commit is blocked until you fix the issue. Because the config is committed to the repo, every contributor runs the same checks.

# Install the framework's git hook for this repo
pre-commit install

# Run all configured hooks against every file once
pre-commit run --all-files

27 Git LFS for Large Files

Git stores every version of every file, which becomes painful for large binaries like videos, datasets or design assets — the repository balloons in size. Git LFS (Large File Storage) is an extension that replaces large files in your repo with tiny text pointers, while the real content lives on a separate LFS server.

You tell LFS which file patterns to track; matching files are transparently swapped for pointers on commit and fetched on checkout. This keeps clones fast and history lean. The tracked patterns are recorded in .gitattributes, which you commit so the whole team uses LFS consistently.

# Track large media with LFS
git lfs install
git lfs track "*.psd"
git add .gitattributes design.psd
git commit -m "Add design asset via LFS"

28 Sparse Checkout in Large Repositories

In a very large repository you may only need a few directories. Sparse checkout lets Git populate just the paths you care about in your working directory while still tracking the full history. Combined with a partial clone (which defers downloading unneeded file contents), it makes working in huge monorepos practical.

You enable sparse checkout in cone mode for simple directory-based patterns, then set which directories to include. Files outside those paths are simply absent from disk but reappear if you widen the set later.

# Clone without checking out everything, then select directories
git clone --filter=blob:none --no-checkout https://github.com/example/mono.git
cd mono
git sparse-checkout init --cone
git sparse-checkout set apps/web libs/shared
git checkout main

29 Conventional Commits and Semantic Versioning

Conventional Commits is a lightweight convention for commit messages that makes them machine-readable. Each message starts with a type such as feat, fix, docs or chore, an optional scope, and a description, e.g. feat(auth): add password reset.

This pairs naturally with Semantic Versioning (MAJOR.MINOR.PATCH): a fix bumps the patch, a feat bumps the minor, and a breaking change (marked with ! or a BREAKING CHANGE: footer) bumps the major. Tools can read these messages to generate changelogs and pick the next version automatically.

# A feature and a breaking change, by convention
git commit -m "feat(api): add pagination to search"
git commit -m "feat(api)!: drop deprecated v1 endpoints"

30 Reusing Conflict Resolutions with rerere

rerere stands for “reuse recorded resolution”. When enabled, Git remembers how you resolved a particular merge conflict. If the same conflict appears again — common when repeatedly rebasing a long-lived branch or re-merging — Git automatically reapplies your earlier resolution.

This saves you from solving the identical conflict by hand over and over. You enable it once via configuration. Git records the conflict and your fix the first time; on later occurrences it stages the remembered resolution, which you can verify before committing.

# Turn on reuse of recorded conflict resolutions
git config --global rerere.enabled true

31 The Git Object Model: Blobs, Trees, Commits and Refs

Under the hood Git is a content-addressable store of four object types:

  • Blob — the raw contents of a file (no name, just data).
  • Tree — a directory listing that maps names to blobs and other trees.
  • Commit — a snapshot pointing to one tree, plus parent(s), author, and message.
  • Tag object — an annotated tag with its own metadata.

Each object is identified by the hash of its content. A ref (like a branch or tag name) is just a friendly pointer to a commit hash. This is why a branch is so cheap: it is a single line of text holding a hash.

# Inspect any object's type and contents
git cat-file -t HEAD
git cat-file -p HEAD

32 Detached HEAD State

Normally HEAD points to a branch name, which in turn points to a commit. When you check out a specific commit, tag, or remote-tracking reference directly, HEAD points straight at that commit instead — this is a detached HEAD.

You can look around and even make commits, but those commits belong to no branch. If you switch away without saving them to a branch, they become unreferenced and may eventually be garbage-collected (though the reflog can still rescue them). To keep work made in a detached HEAD, create a branch with git switch -c before leaving.

# Land in detached HEAD, then save work onto a branch
git checkout a1b2c3d
# ...make commits...
git switch -c experiment

33 Fast-Forward vs No-Fast-Forward Merges

When you merge a branch that is simply ahead of the target, Git can fast-forward: it slides the branch pointer forward with no merge commit, leaving a linear history. This is tidy but hides the fact that the commits came from a feature branch.

Passing --no-ff forces Git to create a merge commit even when a fast-forward was possible. Many teams prefer this so each feature appears as one grouped, easily revertible unit in history. Conversely, --ff-only refuses to merge unless a fast-forward is possible, guaranteeing no merge commits ever appear.

# Always record a merge commit for the feature
git merge --no-ff feature/login

# Refuse to merge unless it can fast-forward
git merge --ff-only origin/main

34 Bare Repositories

A bare repository has no working directory — it contains only the Git database (the contents normally found inside .git). You cannot edit files in it directly. Its purpose is to be a shared remote that people push to and pull from.

Hosting servers store your project as a bare repo. Pushing to a non-bare repo’s checked-out branch is problematic because the working directory would get out of sync, so the canonical “central” copy is always bare. By convention these are named with a .git suffix, such as project.git.

# Create a shared, working-directory-less repo to push to
git init --bare /srv/git/project.git

35 Repository Maintenance: gc and filter-repo

Over time a repository accumulates loose objects and stale references. git gc (garbage collection) compresses objects into packfiles, prunes unreachable objects past their expiry, and tidies refs to keep the repo fast and small. Modern Git can run this automatically via git maintenance.

Sometimes you must rewrite history wholesale — for example to purge a leaked secret or a giant file from every commit. The recommended tool is git filter-repo (a faster, safer successor to filter-branch). Because it rewrites every affected commit hash, everyone must re-clone afterwards.

# Tidy and compress the local repository
git gc --prune=now

# Remove a secret file from all of history
git filter-repo --path secrets.txt --invert-paths

36 Subtrees: Vendoring Without Submodule Pain

A subtree merges another repository’s contents into a subdirectory of your project as ordinary files, rather than as a pointer the way a submodule does. After git subtree add, contributors see and clone the files normally — no extra init step is needed.

You can later pull upstream updates into that subdirectory, and even push local changes back to the source repository. The trade-off versus submodules: subtrees embed the actual history and files (larger repo) but are simpler for collaborators, whereas submodules keep histories separate but add setup friction.

# Vendor a library into a subdirectory, then update it later
git subtree add --prefix=libs/lib https://github.com/example/lib.git main --squash
git subtree pull --prefix=libs/lib https://github.com/example/lib.git main --squash

37 Advanced log, blame and pickaxe

Beyond a basic git log, Git offers powerful ways to investigate history:

  • git blame annotates each line of a file with the commit, author and date that last changed it — great for finding who introduced a line and why.
  • The pickaxe git log -S<string> finds commits that added or removed a given string, helping you track when code appeared or vanished.
  • git log -p shows the patch (diff) for each commit, and --follow tracks a file across renames.

These turn history from a flat list into a searchable investigation tool.

# Who last touched each line, and when did a string appear?
git blame app.js
git log -S "calculateTax" --oneline

🎓 Certificate of Completion

🔒 Complete every lesson quiz above with 90%+ to unlock your downloadable certificate.