Beginner 11 min · March 06, 2026

Git Force-Push Disaster — Stale Rebase Recovery

A force-push with stale refs deleted merged features from main.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Every commit is a permanent, timestamped snapshot with a unique SHA.
  • Git is local-first — all operations (branching, committing, history) work without a network.
  • Branches are lightweight pointers — creating one costs almost nothing.
  • Working directory: your actual files.
  • Staging area (index): what you've selected for the next commit.
  • Repository (.git): the complete history graph.
  • Remote: a shared copy hosted elsewhere (GitHub, GitLab).
  • Git stores content as snapshots, not diffs — this is why branching is instant and history traversal is fast.
  • The reflog is your safety net — it records every HEAD movement for 90 days, allowing recovery of "deleted" commits.
  • Force-pushing to shared branches destroys history and breaks teammates' local repos.
✦ Definition~90s read
What is Introduction to Git?

Before you run a single Git command, you need to understand the word 'repository' — because it comes up constantly. A repository (usually shortened to 'repo') is just a folder on your computer that Git is actively watching and tracking. That's it. The moment you tell Git to watch a folder, that folder becomes a repository.

Imagine you're writing a school essay and you keep saving copies — 'essay_v1.docx', 'essay_FINAL.docx', 'essay_FINAL_v2_ACTUALLY_FINAL.docx'.

Inside every Git repository, Git creates a hidden folder called .git. This is Git's private notebook where it stores the entire history of your project — every version of every file, every message you attached to your saves, and every branch you've ever created.

You'll almost never need to touch that .git folder directly, but knowing it exists explains the magic: as long as that folder is there, your full history is safe.

Think of the repository as a library and the .git folder as the librarian's filing cabinet in the back room. Your actual files are the books on the shelves that you read and edit. The filing cabinet tracks every edition of every book ever checked in, who edited it, and what changed between editions. You work with the books; Git manages the filing cabinet automatically.

Creating a repository is the very first step in any Git workflow. You'll either initialise a fresh one from scratch, or clone (download) an existing one from a platform like GitHub. Let's start from scratch.

Plain-English First

Imagine you're writing a school essay and you keep saving copies — 'essay_v1.docx', 'essay_FINAL.docx', 'essay_FINAL_v2_ACTUALLY_FINAL.docx'. That folder is a mess, and if you accidentally delete the good version, it's gone. Git is a smarter system that automatically tracks every change you make to your code, lets you travel back in time to any previous version, and lets multiple people work on the same project without overwriting each other's work. Think of it like a magical 'undo history' that never expires and works for entire projects, not just single files.

Git is the version control system underlying virtually every software project. It solves three problems: tracking who changed what and when, enabling parallel development through branches, and providing a mechanism to recover from mistakes.

The common misconception is that Git is just a backup system. It is not. Git is a content-addressable DAG (directed acyclic graph) of snapshots. Understanding this mental model separates engineers who blindly run commands from those who can recover from any state, resolve complex conflicts, and design efficient branching strategies.

This guide covers the fundamentals, then layers on production-grade insights: when rebases go wrong, how to recover from force-push disasters, and why your branching strategy directly impacts deployment velocity.

What Is a Repository and Why Does Git Need One?

Before you run a single Git command, you need to understand the word 'repository' — because it comes up constantly. A repository (usually shortened to 'repo') is just a folder on your computer that Git is actively watching and tracking. That's it. The moment you tell Git to watch a folder, that folder becomes a repository.

Inside every Git repository, Git creates a hidden folder called .git. This is Git's private notebook where it stores the entire history of your project — every version of every file, every message you attached to your saves, and every branch you've ever created. You'll almost never need to touch that .git folder directly, but knowing it exists explains the magic: as long as that folder is there, your full history is safe.

Think of the repository as a library and the .git folder as the librarian's filing cabinet in the back room. Your actual files are the books on the shelves that you read and edit. The filing cabinet tracks every edition of every book ever checked in, who edited it, and what changed between editions. You work with the books; Git manages the filing cabinet automatically.

Creating a repository is the very first step in any Git workflow. You'll either initialise a fresh one from scratch, or clone (download) an existing one from a platform like GitHub. Let's start from scratch.

01_init_repository.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Step 1: Create a new project folder and navigate into it
mkdir my-first-project
cd my-first-project

# Step 2: Tell Git to start tracking this folder
# This creates the hidden .git folder inside my-first-project
git init

# Step 3: Confirm that Git is now watching this folder
# The .git folder only appears when you pass the -a flag (show hidden files)
ls -a

# Step 4: Check the current state of the repository
# At this point there are no files yet, so Git will say it's a fresh repo
git status
Output
Initialized empty Git repository in /Users/yourname/my-first-project/.git/
. .. .git
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track them)
Watch Out: Never Run git init in Your Home Directory
Running 'git init' in the wrong folder is a common beginner mistake. If you accidentally run it in your home directory (~), Git will try to track every file on your entire computer. Always create a dedicated project folder first, navigate into it, and then run git init. If you make this mistake, delete the .git folder inside the wrong directory with: rm -rf .git
Production Insight
The .git directory grows over time as you accumulate commits, branches, and objects. In large repositories (monorepos, projects with large binary assets), the .git folder can exceed several gigabytes. Use git count-objects -vH to monitor size. If the repository becomes unwieldy, tools like git gc (garbage collection) and git filter-repo (history rewriting) are essential maintenance operations, not optional cleanup.
Key Takeaway
A repository is a folder with a .git directory that stores the complete object graph of your project's history. The .git folder is the single source of truth — lose it and you lose everything not pushed to a remote. Always push to a remote regularly.
Initializing vs Cloning a Repository
IfStarting a brand new project from scratch.
UseUse git init in an empty directory.
IfJoining an existing project hosted on GitHub/GitLab.
UseUse git clone <url> — this creates the directory, downloads all history, and sets up the remote automatically.
IfNeed a shallow copy for CI/CD (don't need full history).
UseUse git clone --depth 1 <url> — fetches only the latest commit, dramatically reducing clone time for large repos.
Git Force-Push Disaster Recovery Flow THECODEFORGE.IO Git Force-Push Disaster Recovery Flow Stale rebase recovery from local reflog to remote fix Force-Push Disaster Overwritten remote history Local Reflog Find lost commits via git reflog Cherry-Pick Recovery Reapply commits to a new branch Rebase Correctly Use --force-with-lease Push Safely Verify before force-push ⚠ Never use git push --force on shared branches Always use --force-with-lease to prevent overwriting others' work THECODEFORGE.IO
thecodeforge.io
Git Force-Push Disaster Recovery Flow
Introduction Git

Your First Commit: How Git Actually Saves Your Work

In Git, saving your work isn't called 'saving' — it's called 'committing'. A commit is a permanent snapshot of your project at a specific moment in time. Think of it like taking a photograph of your entire project folder. You can take as many photographs as you want, and you can always look back at any photo from any point in the past.

Here's the part that trips up every beginner: Git uses a two-step process to save work, and for good reason. First you 'stage' your changes (step 1), then you 'commit' them (step 2). Staging is like putting items into a box before you seal and label it. It lets you choose exactly which changes go into a commit — maybe you changed three files but only want to snapshot two of them right now. That's completely valid.

The command for staging is git add. The command for committing is git commit. Every commit requires a message — a short human-readable note explaining what changed and why. These messages are invaluable six months later when you're trying to remember why a certain change was made. Write them like you're leaving a note for your future self, because you are.

The analogy: staging is packing the box, committing is sealing it, labelling it, and putting it on the archive shelf permanently.

02_first_commit.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# We're inside my-first-project (from the previous step)

# Step 1: Create a real file with some content
echo "# My First Project" > README.md
echo "This project is tracked by Git." >> README.md

# Step 2: Check what Git sees — it notices the new file but isn't tracking it yet
# Git calls untracked files 'untracked' — they exist in the folder but not in Git's history
git status

# Step 3: Stage the file — tell Git 'yes, include this file in the next snapshot'
# The dot (.) means 'stage everything in the current folder'
git add README.md

# Step 4: Check status again — README.md is now in the 'staging area'
git status

# Step 5: Commit the staged file with a descriptive message
# -m lets you write the message inline — always use present tense, e.g. 'Add' not 'Added'
git commit -m "Add README with project description"

# Step 6: View the commit history — you'll see your commit with its unique ID
git log --oneline
Output
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
[main (root-commit) a3f92c1] Add README with project description
1 file changed, 2 insertions(+)
create mode 100644 README.md
a3f92c1 Add README with project description
Pro Tip: Write Commit Messages Like a Search Engine
A commit message like 'fixed stuff' is useless when you're debugging at 2am six months later. Write messages that answer 'what does this commit do?' in plain English. Good examples: 'Fix login button not responding on mobile', 'Add user authentication with JWT tokens', 'Remove deprecated payment API calls'. Your team — and your future self — will thank you.
Production Insight
The staging area is a powerful but underused tool for precision. In production, you often fix multiple issues in one session. Using git add -p (patch mode) lets you stage individual hunks of a file, creating atomic commits that isolate concerns. This is critical for code review — reviewers can understand each commit independently, and git bisect can pinpoint exactly which change introduced a regression. Squashing unrelated changes into one commit is a debugging liability.
Key Takeaway
Commits are immutable snapshots identified by SHA. The two-step add-then-commit workflow exists for precision — it lets you craft atomic, reviewable changesets. In production, commit atomicity directly impacts debuggability via git bisect and code review clarity. Use git add -p for surgical staging.
Staging Strategy for Clean History
IfYou changed one file for one purpose.
UseUse git add <file> to stage the entire file.
IfYou changed one file for multiple unrelated purposes (e.g., fixed a bug AND added a feature).
UseUse git add -p to interactively stage individual hunks, then commit each concern separately.
IfYou want to stage all tracked file modifications (but not new untracked files).
UseUse git add -u instead of git add . to avoid accidentally staging build artifacts.
IfYou staged something by mistake.
UseUse git reset HEAD <file> to unstage without losing changes.

Branches: How to Experiment Without Breaking Anything

Here's where Git goes from 'useful' to 'genuinely magical'. A branch is an independent line of development that runs parallel to your main code. You can create a branch, make all kinds of experimental changes in it, and your main code is completely untouched. If the experiment works, you merge the branch back in. If it doesn't, you delete the branch and it's as if it never happened.

Every Git repository starts with one default branch, usually called main (older projects may call it master). This is your stable, production-ready code. Nobody should push broken code directly to main. Instead, every new feature or bug fix gets its own branch.

Picture a river. The main river is your main branch — it keeps flowing steadily. When you want to try something new, you dig a side canal (a new branch). You do all your experimental digging in the canal. If the canal works great, you reconnect it to the main river (merge). If it floods and turns into a swamp, you just fill it back in (delete the branch) — the main river never knew anything happened.

This is the exact workflow used at every professional software company on earth. Features developed on branches, reviewed, then merged. Understanding this is what separates someone who 'knows some Git commands' from someone who actually understands Git.

03_branches.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# We're continuing inside my-first-project with our first commit already made

# Step 1: See all existing branches — the asterisk (*) shows your current branch
git branch

# Step 2: Create a new branch called 'add-contact-page'
# Naming convention: use lowercase, hyphens, descriptive names — never spaces
git branch add-contact-page

# Step 3: Switch to the new branch so our changes happen there, not on main
git switch add-contact-page

# (Older Git versions use 'git checkout add-contact-page' — both work)

# Step 4: Create a new file on this branch
echo "# Contact Page" > contact.md
echo "Email us at: hello@myproject.com" >> contact.md

# Step 5: Stage and commit the new file — this commit lives ONLY on add-contact-page
git add contact.md
git commit -m "Add contact page with email address"

# Step 6: Switch back to main and notice contact.md is GONE from your folder
# It's not deleted — it simply doesn't exist on the main branch yet
git switch main
ls

# Step 7: Merge the work from add-contact-page into main
# Now contact.md will appear in main and the commit history merges too
git merge add-contact-page
ls

# Step 8: Delete the branch — the work is merged, we don't need it anymore
git branch -d add-contact-page
Output
* main
Switched to branch 'add-contact-page'
[add-contact-page 7b12e4f] Add contact page with email address
1 file changed, 2 insertions(+)
create mode 100644 contact.md
Switched to branch 'main'
README.md
Updating a3f92c1..7b12e4f
Fast-forward
contact.md | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 contact.md
README.md contact.md
Deleted branch add-contact-page (was 7b12e4f).
Interview Gold: What Is a Fast-Forward Merge?
When you merge and Git says 'Fast-forward', it means main had no new commits since you branched off — so Git simply moves the main pointer forward to your branch tip. No merge commit is created. This is the cleanest type of merge. If both branches had new commits, Git creates a 'merge commit' to tie the two histories together. Interviewers love asking about this distinction.
Production Insight
Branches are cheap pointers, but branch strategy has massive production impact. A team of 20 engineers with undisciplined branching creates merge hell. Trunk-based development (short-lived branches, merge daily) reduces integration risk. Git Flow (feature -> develop -> release -> main) adds structure but increases merge conflict probability for long-lived branches. The trade-off: trunk-based requires comprehensive automated tests; Git Flow tolerates weaker test suites but adds process overhead. Choose based on your team's test coverage, not preference.
Key Takeaway
Branches are lightweight pointers enabling parallel development with zero risk to stable code. The branch strategy (trunk-based vs Git Flow) is a team decision with direct impact on integration frequency, conflict rate, and deployment velocity. No strategy compensates for weak automated tests.
Branching Strategy Selection
IfSmall team, strong test suite, continuous deployment.
UseTrunk-based development. Short-lived feature branches (< 1 day), merge to main frequently. Use feature flags for incomplete work.
IfLarger team, scheduled releases, moderate test coverage.
UseGit Flow or GitHub Flow. Feature branches merge to develop, release branches cut for stabilization, main holds production-ready code.
IfSolo developer or small team, infrequent releases.
UseGitHub Flow. All work on feature branches, merge to main via PR, deploy from main. Simpler than Git Flow.
IfMonorepo with multiple services.
UseTrunk-based with CODEOWNERS. Use path-based review requirements and service-specific CI triggers.

Configuring Git and Connecting to GitHub — The Full Picture

Git runs entirely on your local machine — everything we've done so far lives only on your computer. That's great for personal version control, but most real projects need a remote home so your team can access the code, or so you don't lose everything if your laptop dies. That's where platforms like GitHub, GitLab, and Bitbucket come in.

Think of your local repository as your personal notebook and GitHub as the shared whiteboard in the office. You do your thinking and drafting in your notebook, then when you're ready, you share your updates to the whiteboard. Your colleagues can pull your updates from the whiteboard into their own notebooks.

Before any of this works, Git needs to know who you are. Every commit is stamped with a name and email address so your team knows who made each change. This is a one-time setup on your machine. After that, you point Git at your remote repository with git remote add, push your work up with git push, and pull your teammates' work down with git pull.

You don't need a GitHub account to learn Git locally, but you'll want one before you share any project or apply for jobs — your GitHub profile is your public portfolio.

04_github_setup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ── PART 1: One-time global configuration ──────────────────────────────────

# Tell Git your name — this appears on every commit you ever make
git config --global user.name "Alex Johnson"

# Tell Git your email — use the same email as your GitHub account
git config --global user.email "alex.johnson@example.com"

# Set VS Code as your default editor (optional but recommended for beginners)
git config --global core.editor "code --wait"

# Confirm your settings look correct
git config --list

# ── PART 2: Connect your local repo to GitHub ───────────────────────────────
# Assumes you've created an empty repo on github.com called 'my-first-project'

# Add the remote — 'origin' is the conventional name for your primary remote
# Replace YOUR_USERNAME with your actual GitHub username
git remote add origin https://github.com/YOUR_USERNAME/my-first-project.git

# Verify the remote was added correctly
git remote -v

# ── PART 3: Push your local commits to GitHub ───────────────────────────────
# -u sets 'origin main' as the default so future pushes just need 'git push'
git push -u origin main

# ── PART 4: Pull the latest changes from GitHub (use this daily) ────────────
# This fetches + merges any changes your teammates pushed since your last pull
git pull origin main
Output
user.name=Alex Johnson
user.email=alex.johnson@example.com
core.editor=code --wait
origin https://github.com/YOUR_USERNAME/my-first-project.git (fetch)
origin https://github.com/YOUR_USERNAME/my-first-project.git (push)
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 381 bytes | 381.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/YOUR_USERNAME/my-first-project.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
Already up to date.
Pro Tip: Use SSH Instead of HTTPS for Fewer Password Prompts
Connecting via HTTPS means GitHub will ask for credentials frequently. Set up an SSH key pair once (ssh-keygen -t ed25519 -C 'your@email.com') and add the public key to your GitHub account settings. Then use the SSH remote URL (git@github.com:USERNAME/repo.git) instead of HTTPS. You'll never type a password again for that machine.
Production Insight
The distinction between git fetch and git pull is critical in production. git fetch downloads remote changes but does not modify your working directory — it's safe to run anytime. git pull is git fetch followed by git merge (or git rebase if configured), which modifies your local branch. In CI/CD pipelines and shared environments, always prefer git fetch first, inspect the changes with git log HEAD..origin/main, then decide whether to merge or rebase. Blind git pull on a dirty working directory can create confusing merge states.
Key Takeaway
Git is local-first; remotes are collaboration layers. git fetch is a read operation (safe, inspectable); git pull is a write operation (modifies history). In production, prefer fetch-then-inspect over blind pull. SSH keys eliminate credential friction for daily workflows.
Syncing with Remote: Fetch vs Pull
IfYou want to see what changed remotely without affecting your work.
UseUse git fetch origin then inspect with git log HEAD..origin/main.
IfYou want to incorporate remote changes into your current branch.
UseUse git pull --rebase origin main to replay your commits on top. Cleaner history than merge.
IfYou have uncommitted changes and need to pull.
UseStash first: git stash, then pull, then git stash pop. Pulling with uncommitted changes can create conflicts that are harder to resolve.
IfYou want your local branch to exactly match remote (discard local commits).
UseUse git fetch origin && git reset --hard origin/main. This destroys local work — use with caution.

Why Git Doesn’t Care About Your Feelings: The Object Model Explained

Most tutorials wave their hands saying Git stores 'snapshots.' That’s a lie. Git stores content-addressable objects—blobs, trees, commits, and tags—each hashed with SHA-1. The hash is the address. Change one bit and you get a different address. That’s how Git detects corruption instantly. It’s also why your branch pointers are just labels on these immutable objects.

When you run git add Git packs your file contents into a blob object and writes it to .git/objects. git commit stitches the current tree (think directory listing) with parent commit references and metadata into a commit object. No file is ever overwritten. Every commit creates new objects. That’s why you can time-travel without breaking history.

Understanding this kills the mystery behind detached HEAD, merge conflicts, and rebase. You’re not moving files—you’re reassigning labels to immutable snapshots. Stop thinking like a file system and start thinking like a content-addressable store. Your mental model is everything.

GitObjectInspection.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — devops tutorial

# Inspect the raw object behind a commit
steps:
  - name: Show internal object type
    run: |
      git init
      echo "production_secret" > deploy_key.pem
      git add deploy_key.pem
      git commit -m "initial deploy key"
      # Get the commit's tree object hash
      git rev-parse HEAD:
      # Output: 4b825dc642cb6eb9a060e54bf899d153036d4a1f
  - name: View raw tree content
    run: |
      git cat-file -p 4b825dc642cb6eb9a060e54bf899d153036d4a1f
      # Output: 100644 blob a1b2c3d4 deploy_key.pem
Output
100644 blob a1b2c3d4 deploy_key.pem
Senior Shortcut:
Run git cat-file -p HEAD to see the raw commit object. It’s the fastest way to grasp Git’s internals.
Key Takeaway
Git is a content-addressable filesystem. Every object’s address is its hash. No hash, no change.

The Three Trees You’re Actually Fighting: Working Directory, Staging, HEAD

You’re not just fighting one tree. You’re wrestling three: the working tree (your filesystem), the staging area (aka index), and the HEAD commit (last committed state). Most of Git’s confusing commands—reset, checkout, restore—just shuffle pointers between these three trees.

A git add copies a file from the working tree to the staging area. A git commit freezes the staging area into a new commit object and moves HEAD. git checkout HEAD -- config.yml overwrites your working tree from the staging area. If you understand this triangle, git reset --soft, git reset --mixed, and git reset --hard stop being magic incantations. Soft moves HEAD only. Mixed moves staging. Hard moves all three.

Next time you accidentally commit credentials to staging, you don’t need to panic. You need to unstage and rewrite. Knowing which tree is polluted tells you exactly which command to fire. Stop guessing.

ThreeTreesReset.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — devops tutorial

# Reset a mistakenly staged config file
steps:
  - name: Accidentally stage production config
    run: |
      echo "DB_PASSWORD=supersecret" >> staging.env
      git add staging.env
  - name: Unstage without deleting working copy
    run: |
      git reset HEAD staging.env
      # Staging (index) resets to HEAD. Working file untouched.
  - name: Verify the damage is contained
    run: |
      git status
      # Output: Changes not staged for commit: staging.env
Output
Changes not staged for commit: staging.env
Production Trap:
Using git reset --hard after an accidental stage also blows away uncommitted work. Always confirm with git status first.
Key Takeaway
Three trees—working, staging, HEAD. Reset targets exactly one. Pick the right level or lose data.

Merging Is a Lie: How Git Actually Resolves Conflicts

Everyone tells you merging is automatic. It’s not. Git performs a three-way merge using the two branch tips and their common ancestor. It only auto-merges files where changes don’t touch the same lines. The second two people edit the same region, Git throws its hands up and says 'your turn.'

A merge conflict is just a file annotated with <<<<<<<, =======, >>>>>>> markers. Git doesn’t resolve semantics—it only cares about text. Your job is to pick the final version and strip the markers. Then git add the file to mark it resolved.

Pro move: use git merge --no-commit --no-ff to inspect the auto-merge before committing. If the diff looks wrong, you abort with git merge --abort. Never trust an automated merge on a production branch without code review. Merging is a mechanical step, not a thinking step. You’re the thinking step.

MergeConflictHandling.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — devops tutorial

# Simulate a conflict on a production config
steps:
  - name: Create divergent branches
    run: |
      git init
      echo "TIMEOUT=30" > app.cfg
      git add app.cfg && git commit -m "initial"
      git checkout -b experimental
      echo "TIMEOUT=60" > app.cfg
      git commit -am "increase timeout"
      git checkout main
      echo "TIMEOUT=90" > app.cfg
      git commit -am "even bigger timeout"
  - name: Attempt merge
    run: |
      git merge experimental
      # Output: Auto-merging app.cfg; CONFLICT
      cat app.cfg
      # Output: <<<<<<< HEAD\nTIMEOUT=90\n=======\nTIMEOUT=60\n>>>>>>> experimental
Output
<<<<<<< HEAD
TIMEOUT=90
=======
TIMEOUT=60
>>>>>>> experimental
Senior Shortcut:
Set git config merge.conflictstyle diff3 to see the common ancestor in conflicts—massively reduces guesswork.
Key Takeaway
Git can’t read your mind. Three-way merge is text-only. When in doubt, abort, diff, and reconvene with your team.

Networking: Git Over SSH vs HTTPS — What Actually Happens on the Wire

You type git push and magic happens. No. Git opens a TCP connection, negotiates a protocol, and transfers objects. If you don't understand the transport layer, you'll spend hours debugging auth failures and timeout errors in production.

SSH uses port 22, authenticates with keys, and gives you a persistent connection. HTTPS uses port 443, relies on credential helpers, and forces token rotation. Choose SSH for scripted pipelines where you control the key pair. Choose HTTPS for CI/CD systems that rotate tokens weekly. Your choice determines what breaks at 2 AM.

The real trap: Git's dumb transport protocol sends full objects over the wire. A 10 MB binary file in your repo gets transferred every push. No delta compression for that. Use .gitattributes with binary markers and git lfs for anything over 1 MB. Your network admin will thank you.

git-networking-config.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — devops tutorial

# .gitconfig — force SSH for all remotes
[url "ssh://git@github.com/"]
  insteadOf = https://github.com/

# Set push buffer to avoid "Connection reset" on large repos
[http]
  postBuffer = 524288000  # 500 MB

# Binary file marker — no deltas
.gitattributes:
  "*.psd binary"
  "*.zip -delta"
Output
git push origin main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 2.10 MiB | 45.00 MiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
Production Trap:
CI/CD runners often use HTTPS with a credential helper. If your token expires mid-deploy, the pipeline fails silently. Use SSH deploy keys — they never expire without manual revokation.
Key Takeaway
Git doesn't care about your feelings, but it cares deeply about port 22 vs 443. Pick one, test it under load, and script your auth.

Scripting: Automate Git Like It Owes You Money

You're not going to type git add . && git commit -m "fixes" for the 500th time. That's what shell scripts are for. A senior engineer automates the boring parts and gets out of the way.

The why: Git hooks run before commits, pushes, and merges. Use pre-commit to lint code, prepare-commit-msg to enforce ticket numbers, and post-merge to reinstall dependencies. Write them in Bash or Python, and check them into your repo under .git/hooks/. But remember — hooks are local by default. Use a shared hooks directory with git config core.hooksPath.

The real power: scripting multi-repo workflows. A production deploy touches five microservices across separate repos. Write a Bash loop that clones, checks out the tag, runs CI, and tags. No manual clicking. No forgetting a repo. Your CTO will assume you're a wizard. You're just a dev who writes for repo in "${REPOS[@]}"; do.

auto-deploy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — devops tutorial

# deploy.sh — multi-repo tag-and-push
repos=("api-gateway" "user-service" "payment-worker")
version="v$(date +%Y%m%d).${BUILD_NUMBER}"

for repo in "${repos[@]}"; do
  git clone "git@github.com:acme/${repo}.git" "/tmp/${repo}"
  cd "/tmp/${repo}"
  git tag -a "${version}" -m "auto-deploy ${version}"
  git push origin "${version}"
  echo "Tagged ${repo} with ${version}"
done

echo "All repos deployed to ${version}"
Output
Tagged api-gateway with v20240315.042
Tagged user-service with v20240315.042
Tagged payment-worker with v20240315.042
All repos deployed to v20240315.042
Senior Shortcut:
Share hooks across your team with a hooks/ directory at repo root, then run: git config core.hooksPath hooks/. No more "works on my machine" excuses.
Key Takeaway
If you do it more than twice, script it. If it involves multiple repos, loop it. Git is a tool, not a hobby.

Docker as a Git-Like Version Control for Environments

Docker containers solve the same root problem as Git: reproducibility. Where Git freezes code in time, Docker freezes an entire operating environment. A Dockerfile is a declarative recipe, much like a Git commit history. Running docker build produces an immutable image, and docker run spawns a container from that image. The key insight: images are built in layers, and each layer is cached — identical to how Git caches objects. If a layer doesn't change, Docker reuses it. This means you can ship an environment that is byte-for-byte identical across dev, CI, and production. Without Docker, Git only guarantees code history, not behavior history. For DevOps, pair a Git tag with a Docker image tag: git tag v1.0; docker build -t app:v1.0 . Now you have time traveling for both code and its runtime.

dockerfile-layer-cache.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — devops tutorial

// Docker layers work like Git objects: cached until base changes
FROM node:18 AS base
COPY package*.json ./
RUN npm install          # layer 1, cached unless package.json changes

FROM base AS build
COPY src/ ./
RUN npm run build       # layer 2, cached unless src/ changes

FROM nginx:alpine AS runtime
COPY --from=build /build /usr/share/nginx/html
# Only ENTRYPOINT and port change trigger rebuild here
EXPOSE 80
Output
Layer caching mirrors Git tree objects: parent hash changes propagate. Only changed layers rebuild — identical to Git's SHA-1 tree hashing.
Production Trap:
Never tag containers with 'latest'. It defeats reproducibility — your staging 'latest' and production 'latest' may differ. Always pin to a Git commit SHA or semantic version.
Key Takeaway
Treat Docker images as Git snapshots for environments: immutable, layered, and taggable.

Kubernetes: Git-Ops as the Only Sanity for Distributed Systems

Kubernetes extends Git's declarative model to infrastructure. You declare desired state in YAML files (Deployments, Services, ConfigMaps) and push them to a cluster. The control loop — like Git's merge mechanism — continuously reconciles actual state toward desired state. The missing link: GitOps. Store all Kubernetes manifests in a Git repository. When you merge a PR, a CI tool (ArgoCD, Flux) automatically applies that config to the cluster. This turns rollback into git revert and auditing into git log. Why this beats manual kubectl: every change is traceable, reviewable, and repeatable. If a node crashes, the cluster self-heals from the declarative state — no SSH, no shell scripts. Kubernetes deployments that lack Git history are like code without version control: eventually someone wonders, 'What changed last Tuesday at 3 AM?'

gitops-deployment.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — devops tutorial

// Git repo holds desired state; cluster pulls changes automatically
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
    git-commit: "a1b2c3d"   # track Git SHA for rollback
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: my-registry/web:a1b2c3d  # matching Git tag
Output
ArgoCD detects drift between Git and cluster state every 3 minutes. A manual kubectl edit gets reverted to Git state — Git wins.
Production Trap:
Never edit Deployments directly via kubectl. That is like making hotfixes on production without git commit — the change disappears on next GitOps sync.
Key Takeaway
Kubernetes without GitOps is just distributed chaos. Treat YAML manifests as code: versioned, reviewed, and reverted through Git.

Skills You’ll Gain

By mastering Git and GitHub, you’ll earn the ability to track every line of code change without fear — reverting mistakes, branching experiments, and merging with surgical precision. You’ll understand the object model so deeply that commands like rebase and cherry-pick become second nature. On the DevOps side, you’ll automate pipelines, manage secrets, and implement GitOps workflows that keep Kubernetes clusters honest. Collaborating via pull requests, resolving real merge conflicts, and signing commits will become routine. Crucially, you’ll stop relying on GUIs and start scripting Git like it owes you money — because in production, speed and accuracy are all that matter.

skills-overview.ymlYAML
1
2
3
4
5
6
7
8
9
// io.thecodeforge — devops tutorial
skills:
  - Track and revert changes with git log, reset, reflog
  - Branch and merge without losing work
  - Configure SSH/HTTPS securely
  - Automate with hooks and aliases
  - GitOps for Kubernetes deployments
  - Resolve conflicts manually
  - Sign commits with GPG
Output
// Skills map to real DevOps scenarios
Why This Matters:
These aren't theoretical — every skill here stops a production outage or saves a team sprint.
Key Takeaway
Git fluency is the single highest-leverage skill for any engineer working with code.

Syllabus

This course is structured like your real workflow — not a boring list of commands. Start with configuring Git and connecting via SSH and HTTPS, then write your first commit while understanding the object model under the hood. Graduate to branching strategies, merge conflicts, and rebasing with real examples. Next, explore Git networking, scripting with hooks, and automating chores. The second half shifts to DevOps: Versioning Docker environments like Git snapshots, and implementing GitOps with ArgoCD for Kubernetes. Each module ends with a production-grade challenge, not a multiple-choice quiz. You’ll finish ready to script, debug, and deploy without hand-holding.

syllabus-git-course.ymlYAML
1
2
3
4
5
6
7
8
9
// io.thecodeforge — devops tutorial
modules:
  - Git Foundations: Config, SSH, First Commit
  - Object Model, Trees, and HEAD in Depth
  - Branching, Merging, and Conflict Resolution
  - Networking: SSH vs HTTPS Under the Wire
  - Scripting and Automation with Hooks
  - Docker as Version Control for Environments
  - Kubernetes GitOps with ArgoCD
Output
// 7 modules, 35+ hours of hands-on work
Production Trap:
Skipping the object model module means you'll hit a merge disaster you can't fix.
Key Takeaway
Each module builds on the last — no fluff, no filler.

Hands-On Learning & Certification

Theory is worthless without a terminal. Every concept here comes with a real Git repository you clone, break, and fix. You’ll create branches, simulate merge conflicts, and recover lost commits using reflog. For DevOps, you’ll automate a Git hook that rejects unclean commits, then deploy a Docker container with versioned tags. Finally, you’ll set up a GitOps pipeline that syncs a Kubernetes cluster with a GitHub repo. Complete all challenges, and you earn a certificate of completion from TheCodeForge — verifiable and shareable. Over 50 million learners have started with Git and GitHub. Join them, not as a beginner, but as someone who understands why Git works.

hands-on-certificate.ymlYAML
1
2
3
4
5
6
7
8
// io.thecodeforge — devops tutorial
hands_on:
  - Clone repo and fix simulated merge conflict
  - Recover lost commit with git reflog
  - Write a pre-commit hook for code standards
  - Tag Docker images with Git SHA
  - ArgoCD: sync app from GitHub to K8s
award: Certificate of Completion (verifiable)
Output
// 5 exercises, 1 final project
Join 50 Million+ Learners:
Start today — because every engineer who ships code needs Git, not just as a tool, but as a mindset.
Key Takeaway
You earn the certificate only by shipping working solutions — not by memorizing flags.
● Production incidentPOST-MORTEMseverity: high

The Force-Push That Deleted a Week of Work

Symptom
Teammates pull main and their local branches suddenly have diverged history. Features that were merged yesterday are missing from the codebase. git log on main shows a linear history that doesn't match what was there before.
Assumption
The remote repository was corrupted or rolled back by an admin.
Root cause
A developer ran git rebase main on their feature branch, then force-pushed their branch. When merging, the merge was fast-forwarded. Later, the same developer rebased main locally against an outdated remote (git fetch was skipped), then ran git push --force-with-lease origin main. The lease check passed because their local ref matched what they last fetched — but they hadn't fetched since teammates pushed. The force-push overwrote main with the stale rebased history.
Fix
1. Do NOT pull — this would propagate the damage locally. 2. Find the last good commit SHA from a teammate's reflog or CI logs. 3. Force-push the correct SHA to restore main: git push --force origin <good-sha>:main. 4. All teammates must run git fetch origin && git reset --hard origin/main to sync. 5. Implement branch protection rules requiring PR reviews and disallowing force-pushes to main.
Key lesson
  • Never force-push to a shared branch. Use --force-with-lease only on personal feature branches.
  • Branch protection rules on main are not optional — they are infrastructure.
  • The reflog on any machine that ever pulled the correct main will have the lost commits. Recovery is possible if you act before the reflog expires (90 days default).
  • Always fetch before rebasing against a remote branch. Stale refs are the root cause of most force-push disasters.
Production debug guideFrom symptom to resolution for real production scenarios.4 entries
Symptom · 01
You're on a detached HEAD and have made commits that aren't on any branch.
Fix
1. Don't panic — the commits exist in the object store. 2. Note the current HEAD SHA from git log --oneline -1. 3. Create a branch pointing to it: git branch rescue-branch HEAD. 4. Switch to it: git switch rescue-branch. Your commits are now safe on a named branch.
Symptom · 02
A merge conflict blocks your merge or rebase.
Fix
1. Run git status to see conflicted files. 2. Open each file — look for <<<<<<<, =======, >>>>>>> markers. 3. Edit the file to keep the correct version (or combine both). 4. Stage the resolved file: git add <file>. 5. Continue: git merge --continue or git rebase --continue. 6. If it's a rebase and you want to abort entirely: git rebase --abort.
Symptom · 03
You committed sensitive data (API keys, passwords) and it's now in the remote.
Fix
1. Rotate the secret immediately — it's compromised regardless of git history. 2. Use git filter-branch or git filter-repo to remove the file from all history. 3. Force-push the cleaned history. 4. Add the file pattern to .gitignore. 5. If the repo is public on GitHub, assume the secret was scraped by bots within minutes.
Symptom · 04
git pull creates unexpected merge commits and pollutes history.
Fix
1. This happens when your local branch has commits that the remote doesn't, and you pull with default merge strategy. 2. Use git pull --rebase to replay your commits on top of the remote changes. 3. Set it permanently: git config --global pull.rebase true. 4. This keeps history linear and clean.
★ Git Recovery Triage Cheat SheetFirst-response commands for when things go wrong. The reflog is your primary recovery tool.
You ran `git reset --hard` and lost uncommitted work.
Immediate action
Check if the files are in the reflog or stash. Uncommitted work may be unrecoverable.
Commands
git stash list
git reflog
Fix now
If stashed: git stash pop. If in reflog: git cherry-pick <sha>. If neither: the work is likely lost. Consider file recovery tools for the filesystem.
A teammate force-pushed and your branch is now diverged from remote.+
Immediate action
Do NOT merge or pull — this compounds the problem.
Commands
git fetch origin
git log --oneline origin/main..HEAD
Fix now
If you have commits to preserve: git rebase origin/main. If you want to match remote exactly: git reset --hard origin/main.
You need to find who introduced a bug and when.+
Immediate action
Use git blame or git bisect to trace the change.
Commands
git blame -L 40,50 src/io/thecodeforge/service/PaymentService.java
git log --all --oneline --grep="payment"
Fix now
For binary search across history: git bisect start, then git bisect bad (current) and git bisect good <old-sha>. Git will guide you to the breaking commit.
Your repo is huge and cloning/pulling is slow.+
Immediate action
Check for large files committed to history.
Commands
git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sort -k3 -n -r | head -20
du -sh .git
Fix now
Use git filter-repo to remove large files from history. Enable partial clone: git clone --filter=blob:none <url>. Add large files to .gitignore and use Git LFS for assets.
ConceptWhat It IsReal-World Analogy
RepositoryA folder Git is tracking, with full history stored in .gitA library with a complete archive of every past edition of every book
CommitA permanent snapshot of your project at a specific momentA dated photograph of your entire project folder, sealed forever
BranchAn independent parallel line of developmentA side canal dug from the main river — experiments happen there, not in the river
Staging AreaA holding zone where you choose what goes into the next commitPacking items into a box before you seal and label it
MergeCombining the history of one branch into anotherReconnecting the side canal back to the main river
Remote (GitHub)An online copy of your repository that your team can accessThe shared whiteboard in the office — everyone reads from and writes to it
git pushSending your local commits up to the remote repositoryCopying your notebook updates onto the shared whiteboard
git pullDownloading commits from the remote into your local repoCopying the shared whiteboard updates back into your notebook
Merge vs RebaseMerge preserves branch history with a merge commit; rebase rewrites history to be linearMerge is tying two ropes together with a knot; rebase is untying one rope and splicing it onto the end of the other
git fetch vs git pullFetch downloads remote changes without modifying working directory; pull fetches then mergesFetch is reading the whiteboard without touching your notebook; pull is reading and immediately copying changes in
ReflogA local log of every HEAD movement — your safety net for recoveryA GPS tracker on your car — even if you drive somewhere and forget how to get back, it remembers every turn

Key takeaways

1
A Git repository is just a normal folder with a hidden .git directory inside it
that directory is Git's entire memory, storing every snapshot of your project ever taken.
2
Committing is a two-step process by design
'git add' stages the exact changes you want, and 'git commit' permanently seals that snapshot with a message — this intentional separation gives you precision control over what goes into each save.
3
Branches let you experiment in complete isolation from your working code
the industry-standard workflow is to never commit directly to main, always branch, then merge after review.
4
Git is local-first
everything works on your machine without internet access. GitHub is a remote host — it's not Git itself, it's a platform that stores a copy of your Git repository online so teams can collaborate.
5
The reflog is your ultimate recovery tool
it records every HEAD movement for 90 days, meaning almost any 'lost' commit can be recovered as long as you act before garbage collection runs.
6
Merge preserves history faithfully; rebase rewrites it to be linear. The choice is a trade-off between auditability (merge) and readability (rebase). Never rebase commits that others have based work on.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Git and GitHub?
02
Do I need to pay for Git or GitHub?
03
What happens if two people edit the same file at the same time — does Git break?
04
What is the difference between merge and rebase, and when should I use each?
05
How do I undo a commit that I already pushed to the remote?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Git. Mark it forged?

11 min read · try the examples if you haven't

Previous
Linux Disk and Storage Management
1 / 19 · Git
Next
Git Branching and Merging