DEV Community

Cover image for Mastering Version Control: A Complete Guide to Git for Developers
Pratham
Pratham

Posted on • Edited on

Mastering Version Control: A Complete Guide to Git for Developers

If you've just read about the pendrive nightmare, you know the old way was chaos. But how does Git actually solve this in a real company environment?

This isn't a boring command reference. I'll walk you through Git the way you'll actually use it at work—from cloning the company repo to getting your code merged into production.

What makes this guide different:

  • Structured around the actual workflow companies use
  • Commands introduced when you need them (Just-in-Time Learning)
  • Undo operations at every step (because mistakes happen)
  • The "why" behind every command, not just the "what"

Let's get started.


Part 1: The Mental Model

What is Git?

Git is a distributed version control system that tracks changes in your code.

In plain English: Git is a time machine + collaboration manager for your code.

"Version Control System" means:

  • It tracks every change you make to your code
  • It remembers what your code looked like yesterday, last week, or last year
  • It lets you go back to any previous version instantly
  • It shows you exactly what changed between versions

"Distributed" means:

  • Everyone on your team has a complete copy of the entire project history
  • You don't need internet to work or save your progress
  • If the server crashes, anyone can restore everything
  • No single point of failure

Why Every Company Uses Git

Reason 1: Time Travel for Your Code

Remember Piyush with 23 "FINAL" folders? That never happens with Git.

# See all previous versions
git log --oneline

# Compare today with last week
git diff HEAD~7

# Go back to any version
git checkout abc123
Enter fullscreen mode Exit fullscreen mode

Reason 2: Safe Collaboration

When two developers edit the same file, Git doesn't silently delete anyone's work. Instead:

  1. Developer A edits checkout.js and commits
  2. Developer B edits checkout.js and tries to push
  3. Git says: "Hold on! There are changes you haven't seen. Let me help you merge them."
  4. Both changes are preserved

Reason 3: Experiment Without Fear

Want to try a risky redesign? Create a branch, experiment freely. If it fails, delete the branch. If it works, merge it in. Main code stays safe either way.

Reason 4: Know Who Did What

git blame payment.js

# Output:
# abc123 (Piyush  2025-12-10) function processPayment() {
# def456 (Hitesh  2025-12-11)   if (amount > 0) {
Enter fullscreen mode Exit fullscreen mode

When a bug appears, you know exactly who to ask.

Reason 5: Work From Anywhere

Before the flight: git pull (get latest code)
On the flight: Work offline, commit progress
After landing: git push (sync with team)

No 100MB ZIP files. No airport WiFi struggles.


Part 2: Setup (Do This Once)

Configure Your Identity

Every commit you make will be stamped with this information:

# Set your name (appears in commits)
git config --global user.name "Your Name"

# Set your email (appears in commits)
git config --global user.email "your.email@company.com"

# Set default branch name to 'main'
git config --global init.defaultBranch main

# Verify your settings
git config --list
Enter fullscreen mode Exit fullscreen mode

Why this matters: When something breaks, the team needs to know who wrote that code—not to blame, but to ask questions.


Part 3: The Company Workflow

This is how professional development teams work. Follow this flow for every feature you build.


Step 1: Clone the Main Repository

What it does: Downloads the complete project
(including all history) to your computer.

On GitHub, navigate to the main page of the repository.

Above the list of files, click  Code.
Enter fullscreen mode Exit fullscreen mode


To clone the repository using HTTPS, under "HTTPS", 
click on copy icon.

To clone the repository using an SSH key, including 
a certificate issued by your organization's 
SSH certificate authority, click SSH, then click on copy icon.

To clone a repository using GitHub CLI, click GitHub CLI, 
then click on copy icon.

Enter fullscreen mode Exit fullscreen mode


Change the current working directory to the location 
where you want the cloned repository.

Type git clone, and then paste the URL you copied earlier.

git clone https://github.com/USERNAME/REPOSITORY-NAME.git
Press Enter to create your local clone.

$ git clone https://github.com/USERNAME/REPOSITORY-NAME.git
> Cloning into `Spoon-Knife`...
> remote: Counting objects: 10, done.
> remote: Compressing objects: 100% (8/8), done.
> remove: Total 10 (delta 1), reused 10 (delta 1)
> Unpacking objects: 100% (10/10), done.

This creates:
project-name/
  ├── all project files
  └── .git/          ← Complete history lives here
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. Creates a folder with the repository name
  2. Downloads all files and complete history
  3. Sets up a connection called "origin" pointing to the company repo.
  4. Checks out the default branch (usually main)

UNDO: Cloned to wrong location?
Just delete the folder and clone again. Nothing on the server is affected.

[!TIP]
SSH vs HTTPS: For repositories you'll push to frequently, use SSH:

git clone git@github.com:company/repo-name.git

Why? SSH uses key-based authentication, so you won't need to enter your password for every push. Set up SSH keys once, and Git remembers you.


Step 1.5: Understanding What You Just Cloned

Now that you have a real repository on your machine, let's understand how Git organizes your work.


Diagram: The Three Stages of Git

Think of it like working in an office:

  • Working Directory = Your desk with all your documents and tools (This is where you actively work on things — all files that exist on your computer.)

  • Staging Area = The stack of papers you put in the “ready to finalize” tray (You choose specific documents you want to prepare for submission or final processing.)

  • Repository = The archive cabinet where final approved documents are stored (This is the permanent record


Exploring the .git Folder

Look inside your cloned project. There's a hidden .git folder that contains everything:

# View the .git folder structure
ls -la .git/

# Output:
.git/
  ├── HEAD              ← "You Are Here" marker
  ├── config            ← Repository settings
  ├── refs/
  │   ├── heads/        ← Local branch pointers
  │   │   └── main      ← Just a text file!
  │   └── remotes/      ← Remote branch pointers
  ├── objects/          ← All your files and commits (compressed)
  └── logs/             ← Reflog (your safety net)
Enter fullscreen mode Exit fullscreen mode

Try this yourself:

# See what HEAD contains
cat .git/HEAD
# Output: ref: refs/heads/main

# See what the 'main' branch points to
cat .git/refs/heads/main
# Output: a1b2c3d4e5f6g7h8i9j0... (40-character commit hash)
Enter fullscreen mode Exit fullscreen mode

[!NOTE]
A branch is just a 40-character text file. That's it. Branches are incredibly lightweight—creating one takes microseconds.


Diagram: Local Repository Structure

┌──────────────────────────────────────────────────┐
│            LOCAL REPOSITORY STRUCTURE            │
└──────────────────────────────────────────────────┘

  📁 my-project/
  │
  ├── 📄 index.html           ┐
  ├── 📄 style.css            ├── Your working files (visible)
  ├── 📄 app.js               ┘
  │
  └── 📁 .git/                     ← HIDDEN FOLDER (all the magic)
      │
      ├── HEAD                     ← "You are on: main"
      │   └─→ refs/heads/main
      │
      ├── refs/heads/
      │   ├── main → abc123        ← Branch pointer (text file)
      │   └── feature → def456     ← Another branch
      │
      ├── objects/
      │   ├── ab/c123...           ← Commit objects
      │   ├── de/f456...           ← Tree objects (folders)
      │   └── 12/3abc...           ← Blob objects (file contents)
      │
      └── logs/
          └── HEAD                 ← Reflog: everywhere HEAD has been
Enter fullscreen mode Exit fullscreen mode

What is HEAD?

HEAD is a pointer that tells Git "where you currently are."

Normal State (Safe):

HEAD ──→ refs/heads/main ──→ Commit abc123
         (branch name)       (actual commit)
Enter fullscreen mode Exit fullscreen mode

When you make a new commit, the branch moves forward automatically:

Before:
HEAD ──▶ main ──▶ C ──▶ B ──▶ A

After:
HEAD ──▶ main ──▶ D ──▶ C ──▶ B ──▶ A
Enter fullscreen mode Exit fullscreen mode

Detached HEAD State (Dangerous):

When you checkout a specific commit instead of a branch:

git checkout abc123    # A specific commit, not a branch name
Enter fullscreen mode Exit fullscreen mode

Now HEAD points directly to the commit, bypassing the branch:

HEAD ──→ Commit abc123
         (no branch involved!)
Enter fullscreen mode Exit fullscreen mode

Why Detached HEAD is Dangerous

The Problem: In detached HEAD, if you make new commits, they have no branch protecting them.

Before (on main):
HEAD → main → C    (where A ← B ← C is the history)

You checkout commit B (detached):

       main → C
              ↑
        A ← B ← C
            ↑
          HEAD (detached - pointing directly to B)

You make a new commit D while detached:

       main → C
              ↑
        A ← B ← C
            ↑
            D ← HEAD

You switch back to main:

       main → C ← HEAD
              ↑
        A ← B ← C
            ↑
            D (ORPHANED! No branch points to it)

Where is D? It has NO REFERENCE pointing to it!
Enter fullscreen mode Exit fullscreen mode

The Rule: Git deletes objects with zero references pointing to them.

Reference Type Example Protects Commits?
Branch name main, feature-x ✅ Yes
Tag v1.0.0 ✅ Yes
HEAD (on a branch) HEADmain → commit ✅ Yes
Detached HEAD HEAD → commit directly ❌ Only while you're there!

The moment you leave a detached commit, it becomes eligible for garbage collection.


The Chain of Custody: Why Old Commits Survive

You might wonder: "If main only points to the latest commit, why aren't old commits deleted?"

Answer: Commits point backward to their parents.

main ──→ C ──→ B ──→ A ──→ (initial)
         │     │     │
       parent parent parent
Enter fullscreen mode Exit fullscreen mode

Git's garbage collector runs a reachability check:

  1. Start at all branch pointers and tags
  2. Follow parent links backward
  3. Mark everything reachable as "alive"
  4. Delete anything not reachable
Reachability Check:
┌─────────────────────────────────────────────────────────────────┐
│  main → C → B → A                                               │
│         ✓   ✓   ✓   All reachable from main = PROTECTED        │
│                                                                 │
│  Orphan commit D (from detached HEAD)                           │
│         ✗   No branch or tag points to it = WILL BE DELETED    │
└─────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The 30-Day Grace Period

Good news: orphaned commits aren't deleted immediately.

Git's reflog keeps a record of everywhere HEAD has been for 30-90 days (depending on configuration).

git reflog

# Output:
e4f5g6h HEAD@{0}: checkout: moving from detached to main
d123456 HEAD@{1}: commit: my orphan commit        ← Still recoverable!
abc123  HEAD@{2}: checkout: moving from main to abc123
Enter fullscreen mode Exit fullscreen mode

Recovery:

# Find the orphan commit hash in reflog
git reflog

# Create a branch to save it
git branch rescue-my-work d123456

# Now it's protected!
Enter fullscreen mode Exit fullscreen mode

[!TIP]
Reflog entries expire after 30 days for unreachable commits and 90 days for reachable ones. Recover lost work promptly!


Quick Fix: Escaping Detached HEAD

If you see this warning:

You are in 'detached HEAD' state...
Enter fullscreen mode Exit fullscreen mode

Option 1: Create a branch to keep your work:

git checkout -b my-new-branch
# Now you're safe—a branch protects your commits
Enter fullscreen mode Exit fullscreen mode

Option 2: Go back to an existing branch:

git checkout main
# Warning: Any commits you made while detached may be lost!
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Your Feature Branch

The Golden Rule: Never work directly on main. Always create a feature branch.

# First, make sure you have the latest main
git checkout main
git pull origin main

# Create AND switch to a new branch
git checkout -b feature/user-authentication

# Or the modern way (Git 2.23+)
git switch -c feature/user-authentication
Enter fullscreen mode Exit fullscreen mode

Why branches?


main:        ──●──●──●──●─────────────────●── (stable, production-ready)
                    \                     /
your-feature:        ●──●──●──●──●──●────     (your work, isolated)
Enter fullscreen mode Exit fullscreen mode
  • Your experiments won't break the main code
  • Multiple features can be developed simultaneously
  • Easy to abandon failed experiments
  • Clean history of what changed for each feature

Branch Naming Conventions:

Prefix Use For Example
feature/ New features feature/dark-mode
bugfix/ Bug fixes bugfix/login-timeout
hotfix/ Urgent production fixes hotfix/security-patch

UNDO: Created branch with wrong name?

git branch -m old-name new-name

Step 3: Work on Your Feature

This is where you spend most of your time. The cycle is: Edit → Stage → Commit → Repeat.


📁 Real-World Example: Building a Login Page

Let's walk through a complete example with actual files and terminal output.

Starting Point: You've created a feature branch and are ready to work.

# You're on your feature branch
git branch
#   main
# * feature/login-page
Enter fullscreen mode Exit fullscreen mode

Step A: Create a new file

Create login.html:

<!-- login.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login to Your Account</h1>
    <form>
        <input type="email" placeholder="Email" />
        <input type="password" placeholder="Password" />
        <button type="submit">Login</button>
    </form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Check what Git sees:

git status

# Output:
On branch feature/login-page
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        login.html

nothing added to commit but untracked files present
Enter fullscreen mode Exit fullscreen mode

Git sees the new file but isn't tracking it yet (red text if using colors).


Step B: Stage the file

git add login.html

git status

# Output:
On branch feature/login-page
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   login.html
Enter fullscreen mode Exit fullscreen mode

The file is now in the staging area (green text), ready to be committed.


Step C: Commit the file

git commit -m "feat: add basic login page structure"

# Output:
[feature/login-page 3f2a1b9] feat: add basic login page structure
 1 file changed, 14 insertions(+)
 create mode 100644 login.html
Enter fullscreen mode Exit fullscreen mode

Step D: Modify an existing file

Now add some CSS. Edit login.html:

<!-- login.html (modified) -->
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
    <link rel="stylesheet" href="login.css">   <!-- NEW LINE -->
</head>
<body>
    <h1>Login to Your Account</h1>
    <form>
        <input type="email" placeholder="Email" />
        <input type="password" placeholder="Password" />
        <button type="submit">Login</button>
    </form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

And create login.css:

/* login.css */
body {
    font-family: Arial, sans-serif;
    display: flex;
    justify-content: center;
    padding-top: 100px;
}

button {
    background: #007bff;
    color: white;
    padding: 10px 20px;
    border: none;
    cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

Check status:

git status

# Output:
On branch feature/login-page
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
        modified:   login.html

Untracked files:
        login.css
Enter fullscreen mode Exit fullscreen mode

Step E: See exactly what changed

git diff login.html

# Output:
diff --git a/login.html b/login.html
index 3f2a1b9..8c4d2e1 100644
--- a/login.html
+++ b/login.html
@@ -3,6 +3,7 @@
 <head>
     <title>Login</title>
+    <link rel="stylesheet" href="login.css">
 </head>
Enter fullscreen mode Exit fullscreen mode

Lines starting with + are additions, - are deletions.


Step F: Stage and commit both files

# Stage both files
git add login.html login.css

git status

# Output:
On branch feature/login-page
Changes to be committed:
        modified:   login.html
        new file:   login.css

# Commit
git commit -m "style: add CSS styling to login page"

# Output:
[feature/login-page 7d4e2f3] style: add CSS styling to login page
 2 files changed, 18 insertions(+), 1 deletion(-)
 create mode 100644 login.css
Enter fullscreen mode Exit fullscreen mode

Step G: View your commit history

git log --oneline -3

# Output:
7d4e2f3 (HEAD -> feature/login-page) style: add CSS styling to login page
3f2a1b9 feat: add basic login page structure
9abc123 (origin/main, main) Previous commit on main
Enter fullscreen mode Exit fullscreen mode

Diagram: Commit History Flow


Now let's look at each individual command in detail:

3.1 Check Status (Do This Constantly!)

git status

# Output shows:
# - Modified files (changed but not staged)
# - Staged files (ready to commit)
# - Untracked files (new files Git doesn't know about)
Enter fullscreen mode Exit fullscreen mode

Pro tip: Compact status format

git status -s

# Output:
#  M src/App.js          # Modified, not staged
# M  src/index.js        # Modified and staged
# ?? src/NewFile.js      # Untracked
# A  src/AddedFile.js    # New file, staged
Enter fullscreen mode Exit fullscreen mode

Why use -s? When you're experienced, the full status output is verbose. -s gives you the same info in one line per file.

Make this a habit. Run git status before and after every operation. It's your dashboard.

3.2 Stage Your Changes

git add moves changes to the staging area—preparing them for commit.

Why staging exists: You might change 10 files but only want to commit 3 of them as one logical change. Staging lets you curate your commits, keeping each one focused on a single purpose.

# Stage specific file (when you want precise control)
git add src/login.js

# Stage multiple files
git add src/login.js src/auth.js

# Stage all changes in current directory
git add .

# Stage part of a file (interactive—powerful!)
git add -p src/login.js
Enter fullscreen mode Exit fullscreen mode

Choosing the Right Staging Command:

Command What It Does When to Use
git add <file> Stages one specific file When you want precise control over what's committed
git add . Stages all changes in current directory and subdirectories Quick staging when all changes belong together
git add -A Stages all changes in entire repository When you made changes in multiple directories
git add -u Stages only already-tracked files When you don't want to add new files
git add -p Interactive staging (select parts of files) When you made multiple unrelated changes in one file

Why git add -p is powerful:

Imagine you fixed a bug AND added a new feature in the same file. With -p, you can commit them separately:

git add -p src/user.js
# Git shows each change and asks:
# y = stage this chunk
# n = don't stage this chunk
# s = split into smaller chunks
# q = quit
Enter fullscreen mode Exit fullscreen mode

Real-world scenario: You're working on login and accidentally fix a typo in a comment. Stage the login code first, commit it, then stage and commit the typo fix separately. Clean history!

UNDO: Staged wrong file?

git reset src/wrong-file.js
# or (Git 2.23+)
git restore --staged src/wrong-file.js

Why two commands? git restore is newer (Git 2.23) and clearer in intent. Both work the same, but restore explicitly says "I'm restoring the staged state."

3.3 Review What You're About to Commit

# See unstaged changes
git diff

# See staged changes (what will be committed)
git diff --staged

# Output shows:
# - lines (deleted)
# + lines (added)
Enter fullscreen mode Exit fullscreen mode

Always review before committing. This prevents "wait, did I change that?" moments.

3.4 Commit Your Changes

A commit is a permanent snapshot of your staged changes.

git commit -m "feat: add user login form validation"
Enter fullscreen mode Exit fullscreen mode

Writing Good Commit Messages:

# ❌ BAD - tells nothing
git commit -m "fixed stuff"
git commit -m "update"
git commit -m "wip"

# ✅ GOOD - clear and searchable
git commit -m "fix: resolve login timeout on slow connections"
git commit -m "feat: add dark mode toggle to settings"
git commit -m "docs: update API authentication examples"
Enter fullscreen mode Exit fullscreen mode

Conventional Commit Format:

Type When to Use
feat: New feature
fix: Bug fix
docs: Documentation only
style: Formatting (no code change)
refactor: Code restructuring
test: Adding tests
chore: Maintenance tasks

Why good messages matter: Six months later, you'll search through history to find when a bug was introduced. Would you rather search through "fixed stuff" or "fix: validate email format before API call"?

UNDO: Made a mistake in last commit?

# Fix the message
git commit --amend -m "correct message here"

# Add a forgotten file to last commit
git add forgotten-file.js
git commit --amend --no-edit

[!WARNING]
Don't amend commits you've already pushed to a shared branch!

3.5 Stash: Temporary Work Storage

Scenario: You're mid-feature, but need to switch branches for an urgent bug fix. You can't commit half-done work.

# Save your work temporarily
git stash push -m "WIP: login form halfway done"

# Your working directory is now clean
git switch main
# ... fix the bug, commit, push ...

# Return to your feature
git switch feature/user-authentication

# Restore your work
git stash pop

# Your half-done work is back!
Enter fullscreen mode Exit fullscreen mode

Stash Commands:

Command What It Does When to Use
git stash Stash all changes Quick save when you need to switch immediately
git stash push -m "desc" Stash with description Recommended — you'll know what's in each stash
git stash list Show all stashes See what you've stashed before
git stash pop Restore AND delete the stash When you're sure you want to apply it
git stash apply Restore but KEEP the stash When you might need the same stash on multiple branches
git stash drop Delete without restoring When you no longer need a stash

Why pop vs apply?

  • Use pop (90% of the time): You stashed, switched, fixed the bug, and now you're back. Pop and continue.
  • Use apply (10% of the time): You have a utility change you want to apply to multiple branches. Apply keeps the stash for reuse.

UNDO: Popped wrong stash?
The stashed changes are now in your working directory. If you need to undo, just stash again or use git checkout -- . to discard.

3.6 View History

# See commit history
git log

# Compact one-line format
git log --oneline

# Last 5 commits with graph
git log --oneline --graph -5

# History of specific file
git log -- src/login.js
Enter fullscreen mode Exit fullscreen mode

UNDO: Need to discard ALL changes and start fresh?

git checkout -- .                    # Discard unstaged changes
# or (Git 2.23+)
git restore .

Step 4: Sync with Main (The Critical Step)

While you were working, your teammates pushed changes to main. Your branch is now outdated. Before pushing, you must sync.

Why this matters: If you try to merge outdated code, conflicts explode during the PR. The person who wrote the code (you) is best equipped to resolve conflicts, not the reviewer.

[!IMPORTANT]
The Golden Rule of Conflicts: You should never leave conflicts for the Senior Dev or Maintainer to solve. They don't know why you changed a specific line of code. If they try to fix your conflict, they might accidentally delete your new logic.

Two Approaches: Rebase vs Merge

The Train Track Analogy (from company-workflow.md):

Approach Visualization History Result
Rebase Pick up your train car and place it at the front of the train Single straight line
Merge Build a side track that loops out and joins back Loop/diamond shapes ("bubbles")

Why it matters: If 20 developers all use merge constantly, the history graph looks like a tangled bowl of spaghetti. Rebase keeps it clean.

Rebase Merge
Command git rebase main git merge main
History Linear (straight line) "Bubbles" (diamonds)
Result Looks like you just wrote code on latest main Preserves when parallel work happened
Team Preference Modern teams prefer Some teams require

Approach A: Rebase (Preferred for Clean History)

# 1. Switch to main and get latest
git checkout main
git pull origin main

# 2. Switch back to your branch
git checkout feature/user-authentication

# 3. Rebase onto main
git rebase main
Enter fullscreen mode Exit fullscreen mode

What rebase does:

BEFORE REBASE:
                 (main moved forward while you worked)
                              ↓
main:       A ── B ── C ── D
                 ↑
                 └── E ── F  ← your-branch (branched from B)

AFTER REBASE:
main:       A ── B ── C ── D
                           ↑
                           └── E' ── F'  ← your-branch

Your commits E and F are "replayed" on top of D (latest main).
E' and F' are NEW commits with different hashes but same changes.
Enter fullscreen mode Exit fullscreen mode

If conflicts occur during rebase:

# Git stops and shows conflicted files
git status

# 1. Open each conflicted file
# 2. Look for conflict markers:
<<<<<<< HEAD
code from main
=======
your code
>>>>>>> your-commit

# 3. Edit to keep what you want, remove markers
# 4. Stage the resolved file
git add resolved-file.js

# 5. Continue rebase
git rebase --continue
Enter fullscreen mode Exit fullscreen mode

UNDO: Rebase going badly?

git rebase --abort    # Cancels rebase, returns to before you started

Approach B: Merge (When Rebase is Banned)

Some teams forbid rebase because it rewrites history. In that case:

# While on your feature branch
git merge main
Enter fullscreen mode Exit fullscreen mode

What merge does:

BEFORE MERGE:
                 (main moved forward while you worked)
                              ↓
main:       A ── B ── C ── D
                 ↑
                 └── E ── F  ← your-branch (branched from B)

AFTER MERGE (you run 'git merge main' on your branch):

main:       A ── B ── C ── D ─────────────────┐
                 ↑                            ↓
                 └── E ── F ────────────── M ← your-branch
                                      (merge commit)

M is a "merge commit" with TWO parents: F (your work) and D (main).
This creates a "diamond" or "bubble" in history.
Enter fullscreen mode Exit fullscreen mode

Resolving merge conflicts: Same process as rebase—edit files, remove markers, git add, then git commit.

UNDO: Merge going badly?

git merge --abort    # Cancels merge, returns to before you started

Step 5: Push to Remote

Your branch is synced with main and conflicts are resolved. Time to upload.

# First-time push (sets upstream tracking)
git push -u origin feature/user-authentication

# Subsequent pushes
git push
Enter fullscreen mode Exit fullscreen mode

If you rebased after already pushing:

Rebase rewrites commit history. If you pushed before rebasing, you need to force push:

git push --force-with-lease origin feature/user-authentication
Enter fullscreen mode Exit fullscreen mode

[!CAUTION]
--force-with-lease is safer than --force. It fails if someone else pushed to your branch, preventing you from overwriting their work.

[!CAUTION]
NEVER force push to main or any shared branch! This rewrites history for everyone.

UNDO: Pushed wrong code?

For pushed commits, use git revert (safe—creates an "undo" commit):

git revert abc123    # Creates new commit that undoes abc123
git push

Don't use git reset on pushed commits—it rewrites history others depend on.


Step 6: Create Pull Request (PR)

On GitHub/GitLab, create a Pull Request to merge your branch into main.

A Good PR Includes:

  • Clear title describing the change
  • Description of what and why
  • Screenshots for UI changes
  • Links to related issues

What Happens Next:

  1. Teammates review your code
  2. They may request changes
  3. You make changes, commit, push (branch auto-updates PR)
  4. Once approved, PR is merged

Step 7: Squash and Merge

This is typically done by the approver (senior dev or team lead), not you.

What "Squash and Merge" does:

YOUR PR COMMITS:
E: "feat: add login form"
F: "fix: typo in label"
G: "style: adjust button padding"
H: "fix: forgot validation"

AFTER SQUASH:
S: "feat: add user authentication (#123)"

All your messy WIP commits become one clean commit on main.
Enter fullscreen mode Exit fullscreen mode

Why squash: Main branch history stays clean and readable. Each commit represents one complete feature, not a stream of consciousness.


Step 8: Cleanup

After your PR is merged:

# Switch to main
git checkout main

# Get the merged changes
git pull origin main

# Delete local feature branch (it's merged, you don't need it)
git branch -d feature/user-authentication

# Delete remote feature branch (optional, GitHub can auto-delete)
git push origin --delete feature/user-authentication
Enter fullscreen mode Exit fullscreen mode

Why -d vs -D?

Flag What It Does When to Use
-d (lowercase) Safe delete — only works if branch is fully merged Normal cleanup after PR merge
-D (uppercase) Force delete — deletes even if not merged Abandoning a failed experiment

Reasoning: Use -d by default. Git will refuse if you'd lose unmerged work. Only use -D when you intentionally want to discard unmerged commits.

Congratulations! You've completed the full company workflow. Repeat for every feature.


Part 4: Recovery and Safety Net

Mistakes happen. Git has your back.

The Reflog: Your Time Machine

The reflog tracks everywhere HEAD has been—even "deleted" commits.

git reflog

# Output:
e4f5g6h HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: my important work    ← "deleted" commit is here!
9876543 HEAD@{2}: commit: previous work
Enter fullscreen mode Exit fullscreen mode

Recover "lost" commits:

# Find the commit hash in reflog
git reflog

# Reset to that point
git reset --hard a1b2c3d

# Your work is back!
Enter fullscreen mode Exit fullscreen mode

[!NOTE]
Reflog entries expire after 30 days for unreachable commits (like orphans) and 90 days for reachable commits. Recover lost work promptly!

The Complete Undo Cheatsheet

# ─────────────────────────────────────────────────────────────
# UNSTAGE A FILE (after git add, before commit)
# ─────────────────────────────────────────────────────────────
git reset <file>
# or (Git 2.23+)
git restore --staged <file>

# ─────────────────────────────────────────────────────────────
# DISCARD CHANGES IN A FILE (before staging)
# ─────────────────────────────────────────────────────────────
git checkout -- <file>
# or (Git 2.23+)
git restore <file>

# ─────────────────────────────────────────────────────────────
# UNDO LAST COMMIT (keep changes staged)
# ─────────────────────────────────────────────────────────────
git reset --soft HEAD~1

# ─────────────────────────────────────────────────────────────
# UNDO LAST COMMIT (keep changes unstaged)
# ─────────────────────────────────────────────────────────────
git reset HEAD~1

# ─────────────────────────────────────────────────────────────
# UNDO LAST COMMIT (DELETE changes completely) ⚠️
# ─────────────────────────────────────────────────────────────
git reset --hard HEAD~1

# ─────────────────────────────────────────────────────────────
# UNDO A PUSHED COMMIT (safe - creates "undo" commit)
# ─────────────────────────────────────────────────────────────
git revert <commit-hash>

# ─────────────────────────────────────────────────────────────
# RECOVER "LOST" COMMITS
# ─────────────────────────────────────────────────────────────
git reflog
git reset --hard <commit-from-reflog>
Enter fullscreen mode Exit fullscreen mode

Understanding Reset Modes

                    Moves HEAD?   Clears Staging?   Clears Working Dir?
--soft                 ✓              ✗                   ✗
--mixed (default)      ✓              ✓                   ✗
--hard                 ✓              ✓                   ✓  ⚠️ DANGEROUS
Enter fullscreen mode Exit fullscreen mode

Reset vs Revert: When to Use Which

Scenario Use Why
Undo local commit (not pushed) git reset Safe to rewrite local history
Undo pushed commit git revert Creates undo commit, preserves history
Nuclear option (reset to remote) git reset --hard origin/main Last resort

Part 5: Common Scenarios

"I committed to the wrong branch!"

# You're on main but should be on feature branch
# You just committed here by mistake

# 1. Create the correct branch (with your commit)
git branch correct-branch

# 2. Reset main back to before your commit
git reset --hard HEAD~1

# 3. Switch to correct branch
git checkout correct-branch

# Your commit is now on the correct branch!
Enter fullscreen mode Exit fullscreen mode

"I have uncommitted changes and need to switch branches!"

# Option 1: Stash (if you want to keep changes)
git stash push -m "WIP: description"
git checkout other-branch
# ... do work ...
git checkout original-branch
git stash pop

# Option 2: Commit (if changes are ready)
git add .
git commit -m "WIP: save progress"
git checkout other-branch
Enter fullscreen mode Exit fullscreen mode

"I'm in detached HEAD state!"

This happens when you checkout a specific commit instead of a branch.

# You see: "You are in 'detached HEAD' state"

# If you want to keep working here, create a branch:
git checkout -b my-new-branch

# If you want to go back to a branch:
git checkout main
Enter fullscreen mode Exit fullscreen mode

"My branch is way behind main and has tons of conflicts!"

# Option 1: Incremental rebase (less overwhelming)
git rebase main
# Resolve conflicts one commit at a time

# Option 2: Squash your commits first, then rebase
git rebase -i HEAD~10    # Squash your 10 commits into 1
git rebase main          # Now only 1 commit to conflict-resolve
Enter fullscreen mode Exit fullscreen mode

Part 6: Quick Reference Card

Daily Commands

git status                    # What's changed?
git add .                     # Stage all
git commit -m "message"       # Save snapshot
git push                      # Upload to remote
git pull                      # Download and merge
git log --oneline -10         # Recent history
Enter fullscreen mode Exit fullscreen mode

Branch Operations

git branch                    # List branches
git checkout -b new-branch    # Create and switch
git checkout main             # Switch to main
git merge feature-branch      # Merge into current
git branch -d old-branch      # Delete branch
Enter fullscreen mode Exit fullscreen mode

Sync Operations

git fetch origin              # Download without merging
git pull origin main          # Download and merge
git rebase main               # Replay commits on main
git push -u origin branch     # First push with tracking
git push --force-with-lease   # Force push (after rebase)
Enter fullscreen mode Exit fullscreen mode

Undo Operations

git restore <file>            # Discard unstaged changes
git restore --staged <file>   # Unstage
git reset --soft HEAD~1       # Undo commit, keep staged
git reset HEAD~1              # Undo commit, unstage
git reset --hard HEAD~1       # Undo and delete ⚠️
git revert <commit>           # Create undo commit
git reflog                    # Find lost commits
Enter fullscreen mode Exit fullscreen mode

Stashing

git stash                     # Save temporarily
git stash pop                 # Restore and delete
git stash list                # Show all stashes
Enter fullscreen mode Exit fullscreen mode

What You've Learned

The Mental Model: Git as time machine + collaboration manager

The Three Stages: Working Directory → Staging → Repository

The .git Folder: What HEAD, refs, and objects actually are

The Company Workflow: Clone → Branch → Work → Sync → Push → PR → Merge → Cleanup

Undo Operations: Reset, revert, restore, and reflog for every situation

Best Practices: Conventional commits, branch naming, never force-push to main


Next Steps

  1. Practice the workflow on a real project
  2. Make mistakes intentionally and practice recovering
  3. Use git status constantly until it's second nature
  4. Read error messages—Git's errors are actually helpful

Pro tip: Every expert developer was once confused by Git. The difference is they kept practicing.

You've got this! 🚀


Have questions? Found this helpful? Let me know in the comments below!

Top comments (0)