All articles
45 articles · updated weekly See our Tools
All articles
Tutorials

Git for Beginners: Commits, Branches, and Merge

Understand Git's mental model — commits, branches, and merge — with the commands you'll actually use every day, no fluff.

COVER · Tutorials

You made a change that broke everything, you don't remember what you edited, and your only backup is a file you emailed yourself yesterday. That has a name: no version control. Git exists so that scenario never has to happen — but the learning curve is steep enough that plenty of developers reach their first job without really understanding the mental model behind it.

What Git actually does

Git is not an automatic backup. It's a system that intentionally records the state of your project at moments you choose — and lets you navigate between those states, work on parallel versions, and merge changes from multiple people without overwriting each other's work.

The mental model that makes everything click: think of each commit as a snapshot of your project. You decide when to take the snapshot, what to include in it, and you write a caption. Git stores every snapshot and knows how to "go back" to any of them. Branches are alternate timelines — you fork at a point, work separately, and eventually merge the timelines back.

Installing and configuring

If you don't have Git installed yet:

# macOS (with Homebrew)
brew install git

# Ubuntu/Debian
sudo apt install git

# Windows: download at git-scm.com

After installing, set your name and email — they appear on every commit you make:

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

Initializing a repository

To start versioning an existing project:

cd my-project
git init

This creates a hidden .git folder — that's where Git stores all history. Never delete it manually.

If you're cloning an existing project (from GitHub, for example):

git clone https://github.com/user/repository.git

The basic cycle: working directory, staging, commit

This is where most people get confused. Git has three states for your files:

  1. Working directory — what you've edited but haven't told Git to save yet
  2. Staging area (index) — what you've marked to include in the next commit
  3. Repository — what's been committed and is part of the history

The flow is always: edit → add to stage → commit.

# Check the current state
git status

# Add a specific file to the stage
git add src/utils.js

# Add everything that changed
git add .

# Create the commit with a message
git commit -m "feat: add input validation"

The staging area exists because commits should be atomic — one cohesive change with a clear reason. If you edited three files but only two relate to what you were solving, you stage those two and commit. The third one waits for the next commit.

How to write a good commit message

Bad commit message: "changes", "fix", "wip".

Good commit message: "fix: handle zero quantity in discount calculation".

The most widely adopted standard is Conventional Commits: type: description in imperative mood. The most common types are feat (new feature), fix (bug fix), refactor (no behavior change), docs, test, chore.

You don't need to follow the standard from day one, but the golden rule is: the message should explain what and why, not how. The code already explains the how.

Branches: working in parallel

Imagine you want to add a new feature but don't want to mess with code that's working. That's what branches are for.

# See which branch you're on
git branch

# Create and switch to a new branch
git checkout -b feature/google-login

# (Modern equivalent, Git 2.23+)
git switch -c feature/google-login

The most common convention is main (or master) for stable code, and descriptive branches for everything in development: feature/name, fix/name, hotfix/name.

While you work on the new branch, main stays untouched. You can switch branches at any time (as long as you've committed your current changes):

git switch main

Merge: joining the timelines

When the feature is ready, you merge the changes back into main:

git switch main
git merge feature/google-login

Git will try to combine the two histories automatically. If the changes don't overlap, it succeeds — this is called a fast-forward or a merge commit depending on the history.

If the same lines were edited in both branches, you'll have a merge conflict. Git pauses and marks the problematic files like this:

<<<<<<< HEAD
return calculateDiscount(value, 0.1);
=======
return calculateDiscount(value, rate);
>>>>>>> feature/google-login

You resolve it manually — decide which version stays (or combine both), remove the markers, then:

git add resolved-file.js
git commit

A conflict isn't a Git error. It's Git saying "these two changes contradict each other — you decide."

Commands you'll use every day

Beyond the basic cycle, a few commands quickly become routine:

# See commit history
git log --oneline

# See what changed before committing
git diff

# See what's staged
git diff --staged

# Discard uncommitted changes in a file
git restore src/utils.js

# Create an alias (shortcut)
git config --global alias.st status

git log --oneline is particularly useful — each commit shows up on one line with a short hash and message. It's easy to scan history without noise.

Remote repositories: GitHub, GitLab, Bitbucket

Git is local by default. To collaborate or back up to the cloud, you connect to a remote repository:

# Add the remote (usually called "origin")
git remote add origin https://github.com/user/repo.git

# Push your commits to the remote
git push origin main

# Pull changes from the remote
git pull origin main

Typical team workflow: pull the latest changes from remote, work on your local branch, and when done, push and open a pull request for someone to review before merging into main.

Before opening the PR, it's worth reviewing the diff of what you're about to submit — it's easy to miss a stray console.log or a change unrelated to what you were solving. When I need to compare versions of a file outside a Git context, I use the Text Diff Checker — paste the before and after, and see the differences highlighted side by side.

Frequently asked questions

Can I undo a commit I already made?

Yes, but it depends on whether you've pushed yet. Locally, you have a few options:

# Undo the commit but keep changes in working directory
git reset --soft HEAD~1

# Undo the commit and unstage the changes
git reset --mixed HEAD~1

# Undo the commit and discard all changes (careful — irreversible)
git reset --hard HEAD~1

If the commit has already been pushed to a remote that others may have pulled from, use git revert instead of reset — it creates a new commit that undoes the previous one, without rewriting history.

What's the difference between merge and rebase?

Merge creates a join commit that preserves the complete history of both branches. Rebase rewrites your branch's commits as if they had been made on top of the destination branch — cleaner linear history, but it rewrites commit hashes.

For beginners: use merge. Once you understand the history model well, evaluate rebase — but never rebase commits that are already on a remote others have used.

What is .gitignore?

A file at the root of the project that tells Git which files to ignore. You don't want to version node_modules/, .env, build output, or local IDE config. The format is simple:

node_modules/
.env
dist/
*.log
.DS_Store

The site gitignore.io generates the file for any stack in seconds.

What's the difference between git fetch and git pull?

git fetch downloads changes from the remote but doesn't apply them to your working directory. git pull does a fetch and then automatically merges. For more control, you can fetch first and inspect the changes before applying — this avoids surprises when the remote changed in unexpected ways.

The mental model that carries everything

Git is confusing at first because most people learn the commands before learning the model. What holds everything together is simple: commits are immutable snapshots, branches are pointers to commits, and merge is combining two lines of history.

With that model in mind, commands like reset, rebase, and cherry-pick start to make intuitive sense — you're not manipulating files, you're manipulating pointers and history.

RD
Author
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
View profile