Skip to main content

Harshal V. LADHE

Git Basics: A Beginner's Guide to Version Control

Master Git's core workflow, from commit to merge.
Published at:
Last updated:
Estimated reading time:12 min read
Series:Git: From Basics to Advanced (1/2)

Introduction

Git is a distributed version control system — every clone of a repository is a full copy of its history, not just a checkout of the latest files. That single design choice explains almost everything else about how Git behaves: why commits are cheap, why branches are cheap, why you can work entirely offline, and why "undoing" something in Git usually means finding a commit rather than restoring from a backup.

This is Part 1 of a two-part series. This post covers the everyday loop you'll use in nearly every session: setting up Git, staging and committing, branching and merging, undoing mistakes, and working with a remote like GitHub. Part 2 picks up from here with rebasing, Git internals, and disaster-recovery workflows.

By the end of this post, you'll be comfortable with:

  • The core mental model: working directory, staging area, repository
  • Creating and cloning repos, and keeping untracked junk out of them with .gitignore
  • Everyday commands: status, add, commit, diff, log
  • Branching and merging, including resolving conflicts
  • Undoing mistakes safely — restore, reset, revert, clean
  • Stashing work-in-progress, and working with remotes (fetch/pull/push)
  • Tagging releases

Let's start with the mental model — everything else is easier once this clicks.

Installing & Configuring Git

Installation

# Debian / Ubuntu
sudo apt install git

# Fedora
sudo dnf install git

# macOS (Homebrew)
brew install git

# Windows
# Download from https://git-scm.com/download/win — includes Git Bash

Verify the install and check the version:

git --version

First-Time Setup

Git needs to know who you are before you can commit — this identity gets baked into every commit you make.

git config --global user.name "Ada Lovelace"
git config --global user.email "ada@example.com"

A few options worth configuring early:

# Default branch name for new repos (Git still defaults to "master" unless you set this)
git config --global init.defaultBranch main

# Your preferred editor for commit messages
git config --global core.editor "code --wait"

# Colored output in the terminal
git config --global color.ui auto

# Rebase (not merge) when you run `git pull` on a tracking branch
git config --global pull.rebase true

Inspecting Config

git config --list                # every effective setting, merged across scopes
git config --list --show-origin  # ...and which file each one came from
git config user.email            # a single key
git config --global --edit       # open ~/.gitconfig directly in your editor

How Git Thinks: The Core Model

Almost every Git command operates on one of three areas:

AreaWhat it isHow you interact with it
Working directoryThe actual files on disk you editAny editor, git checkout, git restore
Staging area (index)A draft of your next commitgit add, git restore --staged, git reset
Repository (.git)The committed history — permanent once committedgit commit, git log, git show
Diagram of Git's three core areas: Working Directory, Staging Area, and Repository. Forward arrows labeled git add and git commit connect them left to right. A looping arrow below, labeled git checkout and git restore, connects the Repository back to the Working Directory.Working DirectoryStaging AreaRepository (.git)git addgit commitgit checkout / git restore

A few core objects and terms you'll see everywhere:

  • Commit — an immutable snapshot of the whole project, identified by a SHA hash, pointing at its parent commit(s).
  • Branch — just a movable pointer to a commit. Creating a branch is instant because nothing is copied.
  • HEAD — a pointer to whichever branch (or commit) you currently have checked out.
  • Tag — a pointer to a specific commit that, unlike a branch, never moves.
  • Remote — a named reference to another copy of the repository, usually on a server (origin by default).

Creating & Cloning Repositories

git init

Turns the current directory into a Git repository by creating a .git folder.

git init                     # initialize in the current directory
git init my-project          # create the directory and initialize it
git init --bare              # a repository with no working directory — used for servers

git clone

Copies an existing repository — its full history, not just the latest snapshot.

git clone https://github.com/user/repo.git
git clone https://github.com/user/repo.git my-folder   # clone into a custom directory name

git clone --branch develop https://github.com/user/repo.git   # check out a specific branch
git clone --depth 1 https://github.com/user/repo.git          # shallow clone: only the latest commit

.gitignore

Tells Git which untracked files to never list in status or offer to add.

# Comments start with #
node_modules/          # ignore a whole directory
*.log                  # ignore every file matching a glob
!important.log         # ...except this one (negation)
/dist                  # leading slash: only at the repo root, not any nested "dist"
build/**/*.map         # ** matches across directory boundaries
git check-ignore -v path/to/file   # debug WHY a file is (or isn't) ignored, and by which rule
git rm --cached already-tracked.log  # stop tracking a file WITHOUT deleting it locally

The Basic Workflow: Status, Add, Commit

git status

Shows the state of the working directory and staging area relative to the last commit.

git status
git status -s     # short format: one line per file, with change-type letters
git status -b     # short format, plus the current branch and ahead/behind counts
git status --ignored   # also list files excluded by .gitignore
$ git status -sb
## main...origin/main [ahead 1]
 M src/index.js
A  src/utils.js
?? notes.txt

git add

Moves changes from the working directory into the staging area.

git add index.js              # stage one file
git add src/ tests/           # stage everything under these directories
git add .                     # stage everything in and below the current directory
git add -A                    # stage everything in the whole repo, including deletions, from any cwd
git add -u                    # stage modifications and deletions only — never new/untracked files
git add -p                    # interactively choose which *hunks* of a file to stage
git add --dry-run .           # preview what would be staged, without staging it

git commit

Records the staged snapshot into history.

git commit -m "Add input validation"
git commit -am "Fix typo"          # -a stages every tracked, modified file first (skips untracked!)
git commit                         # opens $EDITOR for a multi-line message
git commit --amend                 # replace the previous commit with a new one (message and/or content)
git commit --amend --no-edit       # amend the content, keep the existing message
git commit -v                      # show the diff being committed inside the message editor

Inspecting Changes: diff, show, and log

git diff

Compares two states of the repository — by default, working directory against the index.

git diff                       # working directory vs. staging area (unstaged changes)
git diff --staged              # staging area vs. last commit (a.k.a. --cached)
git diff HEAD                  # working directory vs. last commit (staged + unstaged together)
git diff main..feature         # a branch against another branch
git diff -- src/index.js       # limit the diff to one path
git diff --stat                # summary: files changed, insertions/deletions, no line-by-line detail

Sample output:

diff --git a/src/greet.js b/src/greet.js
index 83db48f..bf269c4 100644
--- a/src/greet.js
+++ b/src/greet.js
@@ -1,3 +1,3 @@
 function greet(name) {
-  return "Hello " + name;
+  return `Hello, ${name}!`;
 }

git show

Displays a single commit — its message and its diff against its parent.

git show HEAD              # the most recent commit
git show abc1234           # a specific commit by (abbreviated) SHA
git show --stat HEAD       # just the changed-files summary

git log

The history browser.

git log                          # full history, newest first
git log --oneline                # one compact line per commit
git log --oneline --graph --all  # ASCII graph of every branch's history — hugely useful
git log -p                       # each commit's full diff inline
git log -n 5                     # limit to the last 5 commits
git log --author="Ada"           # filter by author (substring match)
git log --grep="fix"             # filter by commit message content
git log -- src/index.js          # history that touched one path

git blame

Shows, line by line, which commit last touched each line of a file.

git blame src/index.js
git blame -L 10,20 src/index.js   # only lines 10–20
git blame -w src/index.js         # ignore whitespace-only changes

Branching

Branches are the feature that makes Git's workflow possible — because a branch is just a pointer, creating one is instant regardless of repo size.

git branch                       # list local branches, * marks the current one
git branch -a                    # local + remote-tracking branches
git branch -v                    # list with each branch's latest commit
git branch feature/login         # create a new branch (doesn't switch to it)
git branch -m old-name new-name  # rename a branch
git branch -d feature/login      # delete a branch (only if fully merged)
git branch -D feature/login      # force-delete, even if unmerged — use with intent

Switching Branches: switch vs. checkout vs. restore

Historically, git checkout did three unrelated jobs — switch branches, restore files, and check out a detached commit — which made it confusing and easy to misuse by accident. Git 2.23 split it into two focused commands.

# Modern (recommended)
git switch feature/login          # switch to an existing branch
git switch -c feature/signup      # create AND switch, in one step
git switch -                      # switch back to the previous branch (like cd -)

# Legacy, still everywhere in older docs/scripts
git checkout feature/login
git checkout -b feature/signup

Merging

Bringing one branch's changes into another.

git switch main
git merge feature/login              # merge feature/login into the current branch
git merge --no-ff feature/login      # always create a merge commit, even if a fast-forward is possible
git merge --ff-only feature/login    # fail instead of merging, unless a fast-forward is possible
git merge --abort                    # bail out of a merge with conflicts, back to the pre-merge state

Fast-Forward vs. Three-Way Merge

Fast-forward — main has no new commits of its own, so the pointer just moves:

Diagram of a fast-forward merge: main's pointer moves forward along feature's existing commits, with no new commit created.mainfeatureABCDgit merge featurepointer just moves — no new commitmainABCD

Three-way merge — both branches have new commits, so a merge commit is created:

Diagram of a three-way merge: main and feature both have new commits, so a new merge commit is created joining both.mainfeatureABECDgit merge featurecreates merge commit M (parents: E, D)mainfeatureABEMCDcommitmerge commit

Resolving a Merge Conflict

When both branches changed the same lines, Git stops and marks the file:

<<<<<<< HEAD
const GREETING = "Hello";
=======
const GREETING = "Hi there";
>>>>>>> feature/login
  1. Open the file, edit it down to the single correct version, and remove the <<<<<<< / ======= / >>>>>>> markers.
  2. git add <file> to mark it resolved.
  3. git commit to finish the merge (the message is pre-filled).
git status                # lists files still marked "both modified"
git diff                  # shows the conflict markers in context
git checkout --ours -- path/to/file    # take "our" side entirely for one file
git checkout --theirs -- path/to/file  # take "their" side entirely for one file
git merge --abort          # start over instead of resolving

Undoing Things

Git has several "undo" commands, and picking the right one depends on where the change lives.

You want to...Use
Discard unstaged changes in a filegit restore <file>
Unstage a file (keep its edits)git restore --staged <file>
Change the last commit's message/contentgit commit --amend
Move HEAD back, keep changes stagedgit reset --soft <commit>
Move HEAD back, unstage changesgit reset --mixed <commit> (the default)
Move HEAD back, discard changes entirelygit reset --hard <commit>
Undo a commit without rewriting historygit revert <commit>
Remove untracked files entirelygit clean

git restore

git restore index.js               # discard unstaged changes, back to the last commit's version
git restore --staged index.js      # unstage the file, but keep its edits in the working directory

git reset

git reset --soft HEAD~1     # undo the last commit, keep everything staged
git reset HEAD~1            # (--mixed, the default) undo the commit, unstage the changes
git reset --hard HEAD~1     # undo the commit AND discard the changes entirely
git reset HEAD -- file.js   # unstage one specific file, keep its edits
Diagram of git reset modes against Working Directory, Staging Area, and Repository. --soft resets only the Repository. --mixed also resets the Staging Area. --hard resets all three, including the Working Directory.WorkingDirectoryStagingAreaRepository(HEAD)--softkeptkeptreset--mixed (default)keptresetreset--hardresetresetresetkept — untouched by this modereset — overwritten to match the target commit

git revert

Creates a new commit that undoes a previous one — safe for already-shared history, unlike reset.

git revert abc1234             # revert one commit, opens editor for the message
git revert --no-edit abc1234   # ...using the default generated message
git revert -n abc1234          # revert into the staging area, but don't commit yet (stack up multiple)

git clean

Removes files Git isn't tracking — separate from reset, which only touches tracked files.

git clean -n     # dry run — show what WOULD be deleted, deletes nothing
git clean -f     # actually delete untracked files
git clean -fd    # ...and untracked directories too

Stashing

Temporarily shelves changes you're not ready to commit, so you can switch context and come back to them later.

git stash                      # stash tracked changes (staged + unstaged)
git stash push -m "wip: login" # same, with a descriptive message
git stash -u                   # also stash untracked files

git stash list                 # see every stash, most recent first
git stash show -p stash@{1}    # view a specific stash's diff

git stash pop                  # re-apply the most recent stash AND remove it from the list
git stash apply                # re-apply the most recent stash, but KEEP it in the list

git stash drop stash@{0}       # delete one stash without applying it
git stash clear                # delete every stash

Working with Remotes

git remote

git remote -v                       # list remotes with their URLs
git remote add origin <url>         # register a new remote named "origin"
git remote set-url origin <url>     # change a remote's URL
git remote show origin              # detailed info: tracked branches, push/pull URLs

git fetch

Downloads commits and refs from a remote — without touching your working directory or local branches.

git fetch origin                # update remote-tracking branches (origin/main, etc.)
git fetch --all                 # fetch from every configured remote
git fetch --prune               # also delete local references to branches deleted on the remote

git pull

Shorthand for fetch followed by either a merge or a rebase into your current branch.

git pull                    # fetch + merge (or rebase, if pull.rebase is configured)
git pull --rebase           # fetch + rebase, just for this one pull
git pull origin main        # explicitly specify remote and branch

git push

git push                              # push the current branch to its configured upstream
git push origin main                  # push explicitly to a remote and branch
git push -u origin feature/login      # push AND set feature/login to track origin/feature/login
git push origin --delete feature/old  # delete a branch on the remote

Tagging

Tags mark specific points in history — releases, most commonly — and unlike branches, they never move.

git tag                          # list all tags
git tag v1.0.0                   # lightweight tag on the current commit
git tag -a v1.0.0 -m "Release 1.0.0"   # annotated tag — has its own message, author, and date
git tag -d v1.0.0                # delete a local tag

git push origin v1.0.0           # push a single tag
git push origin --tags           # push every local tag

git checkout v1.0.0              # check out a tag (detached HEAD)

Command Cheat Sheet

A quick-reference pass over everything in this post.

TaskCommand
Configure identitygit config --global user.name/user.email
Start a repogit init / git clone <url>
Check statusgit status -sb
Stage changesgit add <path> / git add -p
Commitgit commit -m "..."
Fix last commitgit commit --amend
View historygit log --oneline --graph --all
Compare changesgit diff / git diff --staged
Create a branchgit switch -c <name>
Merge a branchgit merge <branch>
Discard unstaged editsgit restore <path>
Unstage a filegit restore --staged <path>
Undo a commit, keep changesgit reset --soft HEAD~1
Undo a shared commitgit revert <sha>
Shelve work-in-progressgit stash / git stash pop
Sync with remotegit fetch / git pull
Publish a branchgit push -u origin <branch>
Tag a releasegit tag -a v1.0.0 -m "..."

Best Practices

Commit early, commit often, commit small. A commit that does one thing is easy to review and easy to revert.

Write commit messages for your future self. A summary line under ~50 characters, imperative mood ("Add" not "Added"), and — for anything non-obvious — a blank line followed by why the change was made, not just what it does.

Pull before you push, fetch before you assume. git fetch is free and safe; run it often so git status -sb's ahead/behind counts are actually trustworthy.

Keep main always deployable. Do your experimenting on a branch. If main breaks, everyone downstream — CI, teammates, deploys — breaks with it.

Use .gitignore from the very first commit. Retrofitting it after node_modules/ or build output has already been committed means untracking files one by one — much easier to get right up front. A minimal one to start from:

node_modules/
dist/
.env
*.log

Common Pitfalls

"I committed to the wrong branch"

git branch correct-branch     # create a branch AT the current (wrong) commit
git reset --hard HEAD~1       # move the original branch back, dropping the commit from it
git switch correct-branch     # your commit is safe here, on the branch it should've been on

"My push was rejected: non-fast-forward"

The remote has commits you don't have locally yet — usually a teammate pushed first.

git pull --rebase     # fetch their commits, replay yours on top
git push

"I have a detached HEAD and don't want to lose this work"

git switch -c rescue-branch   # immediately save the current commit under a real branch name

Frequently Asked Questions (FAQ)

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

fetch only downloads — it never touches your working directory or current branch. pull is fetch followed immediately by a merge (or rebase, if configured) into your current branch.


Can I rename a file without losing its history?

Yes — Git detects renames automatically based on content similarity; you don't need a special "rename" command. git mv old.js new.js (which is really just mv + git add + git rm) works fine too. git log --follow -- new.js will show history from before the rename.


Why does git status show a file as modified right after I clone?

Almost always line-ending normalization (core.autocrlf) rewriting the file on checkout to match your platform's convention. git diff on that file will show only whitespace/line-ending changes, not real content changes.

Conclusion

That's the everyday Git loop: three areas your files move through (working directory → staging → repository), commits as immutable snapshots, and branches as cheap, movable pointers. Once that model is solid, staging, committing, branching, and merging stop being memorized commands and start being things you can reason your way through.

Key Takeaways:

  • Git tracks three areas: working directory, staging area (index), and the committed repository — know which one a command operates on
  • Commits are snapshots with parent pointers, not diffs — diffs are computed for display
  • Branches are cheap because they're just a pointer to a commit, nothing more
  • For undoing: restore (working dir/staging), reset (move HEAD, local-only), revert (new commit, safe for shared history)
  • .gitignore only affects untracked files — untrack an already-committed file with git rm --cached before an ignore rule can take effect
  • fetch is always safe; pull combines it with a merge or rebase
  • A resolved merge conflict is just add + commit, same as any other change

🔗 Up Next (Part 2): Advanced Git: Rebasing, Internals, and Real-World Workflows — where we'll cover:

  • Rewriting history safely with interactive rebase
  • Cherry-picking, bisecting, and finding "lost" work with reflog
  • What a commit, branch, and tag actually are on disk
  • Submodules, worktrees, hooks, and the branching workflows real teams use

Stay tuned — you're one post away from being genuinely dangerous with Git. 💪

This one's meant to be the guide I wish I'd had when Git still felt like a wall of unfamiliar commands — I hope it makes that first `git merge` conflict feel like a puzzle instead of a crisis. Part 2 picks up right where this leaves off. 🌱

  • Advanced Git: Rebasing, Internals, and Real-World Workflows

    Rebase confidently, understand internals, and recover from anywhere.
    An advanced Git guide covering interactive rebasing, cherry-picking, bisect, Git internals (objects, refs, the .git directory), submodules, worktrees, hooks, reflog-based recovery, and real branching workflows.
    Published at:
  • Getting Started with CSS Grid: A Beginner's Guide to 2D Layouts

    Lay the foundation, shape the grid.
    A beginner-friendly introduction to CSS Grid that covers core concepts and guides you through building your first 2D layouts with real-world examples.
    Published at:
  • Mastering CSS Grid: Grid Areas, Item Alignment, and Spanning

    Structure with clarity, align with precision.
    Take your CSS Grid skills to the next level by mastering semantic layouts with grid areas, named lines, item alignment, spanning, subgrid, and accessible ordering.
    Published at: