Version control with Git starts simple — add, commit, push. But the deeper you go into real-world projects, the more complicated collaboration becomes. You make commits you regret, forget to include files, end up with messy histories, or lose work after a reset.

At this point, basic Git isn’t enough. You need to know its advanced features — the ones that allow you to rewrite history, squash commits, recover lost work, and synchronize branches without cluttering your logs.

This blog is divided into four detailed sections, each covering one advanced area of Git with step-by-step commands, real outputs, and practical tips:

  1. Commit History Rewriting (Amend & Interactive Rebase)
  2. Branch Rebase (Linearizing History)
  3. Remote Workflows (Fetch, Pull, and Pull with Rebase)
  4. Recovering Work with Reflog

By the end, you’ll not only understand how these commands work but also when and why to use them in collaborative projects.

Section 1: Commit History Rewriting

One of the most powerful aspects of Git is the ability to rewrite commit history. While this can be dangerous if misused, it’s invaluable for cleaning up mistakes, improving readability, and polishing your repository before sharing.

1.1 Amending Commits

Amending allows you to update the last commit instead of creating a new one. This is useful if you forgot to add a file, want to tweak a commit message, or need to combine corrections into the most recent commit.

The key point: amending rewrites history by generating a new commit SHA. That means you should amend only commits that haven’t been pushed yet.

Example: Adding a File to the Last Commit

Let’s say your log looks like this:

git log --oneline
0832375 (HEAD -> main, origin/main) HTML File
f86213d first commit

You commit a change to blog.html:

git commit -am "main: modified the title - blog.html"
[main ab40261] main: modified the title - blog.html
2 files changed, 9 insertions(+), 1 deletion(-)

git log --oneline
ab40261 (HEAD -> main) main: modified the title - blog.html
0832375 (origin/main) HTML File
f86213d first commit

But now you realize that you also need to include 404.html in the same commit.

git add 404.html
git commit --amend --no-edit

[main 4bf9f69] main: modified the title - blog.html
Date: Thu Sep 11 20:15:36 2025 +0200
3 files changed, 60 insertions(+), 1 deletion(-)
create mode 100644 404.html

Notice how the SHA changed from ab40261 to 4bf9f69. The old commit is gone; the new one includes both blog.html and 404.html.

Example: Amending Only the Message

If you just want to fix the commit message:

git commit --amend -m "main: updated blog.html and added 404.html"

Or open your editor:

git commit --amend

This replaces the message while leaving the content intact.

Safe Rule of Thumb:

1.2 Interactive Rebase for Squashing

Sometimes you make multiple small commits that should really be one. For example:

git log --oneline
a1b2c3d Fix typo
d4e5f6g Update config
h7i8j9k Add feature

Instead of merging these as-is, you can squash them with interactive rebase:

git rebase -i HEAD~3

This opens a text editor:

pick h7i8j9k Add feature
pick d4e5f6g Update config
pick a1b2c3d Fix typo

Change the last two pick commands to squash:

pick h7i8j9k Add feature
squash d4e5f6g Update config
squash a1b2c3d Fix typo

After saving, Git combines them into one commit. The result is a cleaner log that highlights the feature rather than the noise of small corrections.

When to use: Always squash before merging a feature branch into main if your commits are too fragmented.

Section 1: Summary

Section 2: Git Rebase — Keeping History Clean

If there’s one Git command that developers both love and fear, it’s rebase. At its core, rebasing means taking your commits and moving them onto a new base commit. Instead of merging, which creates a new “merge commit,” rebasing replays your commits as if they were made directly on top of the latest branch.

The result? A linear history that is easier to read and follow.

2.1 Rebase vs Merge

Both merge and rebase solve the same problem: they integrate changes from one branch into another. But the way they do it creates very different histories.

Merge: Adds a new merge commit. History keeps all the branches and merges.

Rebase: Moves your commits to the tip of another branch. Creates a straight line of commits.

Think of merge as “recording exactly how the work happened” and rebase as “presenting the history as if it all happened in order.”

2.2 Performing a Basic Rebase

A simple rebase looks like this:

git checkout feature
git rebase main

This tells Git: “Take the commits from the feature branch and reapply them on top of the latest commit from main.”

If there are no conflicts, the rebase finishes automatically.

2.3 Handling Rebase Conflicts (Step-by-Step Example)

Rebases often trigger conflicts when both branches have modified the same lines. Let’s simulate a real-world scenario.

Step 1: Create a feature branch

git checkout -b feature-branch

Output:

Switched to a new branch 'feature-branch'

Both main and feature-branch currently point to the same commit.

Step 2: Add commits to the feature branch

git commit -am "feature-branch: modified title - 404.html"
[feature-branch 82ff182] feature-branch: modified title - 404.html

git commit -am "feature-branch: modified Error Message - 404.html"
[feature-branch e7f8a2e] feature-branch: modified Error Message - 404.html

Now feature-branch has two unique commits.

Step 3: Add conflicting commits to main

git checkout main
git commit -am "main: modified title again - 404.html"
[main 84900fe] main: modified title again - 404.html

git commit -am "main: modified Error Message again - 404.html"
[main 9cbea38] main: modified Error Message again - 404.html

Visual log:

* 9cbea38 (main) main: modified Error Message again - 404.html
* 84900fe main: modified title again - 404.html
| * e7f8a2e (feature-branch) feature-branch: modified Error Message - 404.html
| * 82ff182 feature-branch: modified title - 404.html
|/

Both branches have diverged.

Step 4: Start the rebase

git checkout feature-branch
git rebase main

Git attempts to replay the commits but hits a conflict:

Auto-merging 404.html
CONFLICT (content): Merge conflict in 404.html
error: could not apply 82ff182...

Step 5: Resolve the first conflict

git mergetool

Output:

Merging:
404.html

Normal merge conflict for '404.html':
{local}: modified file
{remote}: modified file

Manually fix the conflict, then:

git add 404.html
git rebase --continue

Step 6: Resolve the second conflict

Git applies the next commit, but another conflict occurs. Fix the file again, then:

git add 404.html
git rebase --continue

Now the rebase completes successfully.

Output:

Successfully rebased and updated refs/heads/feature-branch.

Step 7: Verify history

git log --oneline --decorate --graph --all

Output:

* 24d3a81 (HEAD -> feature-branch) feature-branch changes
* 9cbea38 (main) main changes
* 84900fe main changes

The feature-branch commits are now sitting on top of main, giving a clean, linear history.

Step 8: Fast-forward merge

Since the branch is rebased, merging it into main is now a simple fast-forward:

git checkout main
git merge feature-branch

Output:

Updating 9cbea38..24d3a81
Fast-forward
404.html | 4 ++--

2.4 Handy Rebase Commands

git rebase --abort
git rebase --skip
git rebase --continue

Section 2: Summary

Section 3: Working with Remote Branches

In collaborative projects, your local repository is just one copy of the truth. The actual “source of truth” often lives in a remote repository such as GitHub, GitLab, or Bitbucket. While you are busy committing locally, teammates may push their own changes. Without synchronization, your main branch risks falling behind, which can lead to merge conflicts or overwritten work.

Git provides three key commands to manage this synchronization: git fetch, git pull, and git pull --rebase. Although they may seem similar at first glance, they each serve distinct purposes and have different effects on history.

3.1 Git Fetch — Updating Remote Awareness

git fetch updates your local knowledge of the remote repository without touching your working branch.

git fetch origin

This downloads new commits and updates the pointer of origin/main, but your main branch remains unchanged. It’s like downloading the latest state of the remote into your inbox without applying the changes.

Practical Example: Detecting Divergence

Check all branches:

git branch -av

Output:

* main                453b2c9 Rebasing Done with Updated Readme
remotes/origin/main 453b2c9 Rebasing Done with Updated Readme

At this point, both branches are in sync.

Suppose teammates push two new commits (Included Hyperparameter tuning and Created svm_classification.py). Your local branch is now behind. Run:

git fetch origin main

Output:

From github.com:SurajBhar/advance_git
* branch main -> FETCH_HEAD
453b2c9..e5c4de0 main -> origin/main

Now origin/main is updated, but main is still old.

Compare logs:

git log --oneline main
453b2c9 Rebasing Done with Updated Readme
git log --oneline origin/main
e5c4de0 (origin/main) Included Hyperparameter tuning
86f6a84 Created svm_classification.py
453b2c9 Rebasing Done with Updated Readme

Finally, fast-forward main to sync:

git merge origin/main

Output:

Updating 453b2c9..e5c4de0
Fast-forward
svm_classification.py | 125 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 125 insertions(+)
create mode 100644 svm_classification.py

Now main and origin/main point to the same commit again.

3.2 Git Pull — Fetch + Merge

If you want Git to both fetch and integrate in one step, use:

git pull origin main

This updates origin/main and immediately merges those changes into your local branch.

Example: Simple Pull

git pull origin main

Output (fast-forward case):

Updating e5c4de0..0535e79
Fast-forward
config.yaml | 10 +++++-----

Now both main and origin/main share the same latest commit.

The downside? If you pull often while also committing locally, you may accumulate many “merge commits” in history that don’t add value.

3.3 Git Pull with Rebase — Cleaner History

Sometimes you want the latest remote changes, but you also want to keep your history linear — as if your commits came after the remote commits.

That’s where rebase comes in:

git pull --rebase origin main

This sequence happens:

  1. Fetch updates from origin/main.
  2. Temporarily remove your local commits.
  3. Apply the new remote commits.
  4. Replay your local commits on top.

Example: Rebasing Local Commits on Top of Remote

Before pull:

git log --oneline --decorate --graph --all
* ebf7c5c (HEAD -> main) main: modified readme - README.md
* e5c4de0 (origin/main) Included Hyperparameter tuning
* 86f6a84 Created svm_classification.py
* 453b2c9 Rebasing Done with Updated Readme

Now, teammates push two more commits:

Run:

git pull --rebase origin main

Output:

From github.com:SurajBhar/advance_git
* branch main -> FETCH_HEAD
e5c4de0..0535e79 main -> origin/main
Successfully rebased and updated refs/heads/main.

After rebase, the history becomes:

git log --oneline --decorate --graph --all
* ebf7c5c (HEAD -> main) main: modified readme - README.md
* 0535e79 (origin/main) Update 2 config.yaml
* d9b0764 Updated 1 config.yaml
* e5c4de0 Included Hyperparameter tuning
* 86f6a84 Created svm_classification.py
* 453b2c9 Rebasing Done with Updated Readme

Notice that your commit (ebf7c5c) now appears on top of the remote commits, making history linear and easier to follow.

3.4 Comparative View

To consolidate the differences, here is a quick reference table:

Section 3: Summary

Section 4: Recovering Work with Git Reflog

One of the most intimidating experiences for a developer using Git is the moment you think you’ve “lost” your work. Maybe you ran a hard reset and wiped out a series of commits, or perhaps a rebase rewrote history in ways you didn’t expect. In most tools, that work would be gone forever. In Git, however, there is a safety net: the reflog.

The reflog, short for reference log, is Git’s internal diary. Every time the tip of a branch changes — whether by commit, checkout, reset, merge, or rebase — Git writes an entry into the reflog. Unlike git log, which shows only the commits reachable in the current branch history, the reflog records all movements of HEAD, even those that have since been abandoned. This means that even if commits “disappear” from git log, they can often still be recovered through the reflog.

4.1 What is the Reflog?

At a high level, reflog is your personal undo history. It is local-only — each developer’s reflog exists in their .git/logs/ directory and is not shared with collaborators. It records every movement of the HEAD pointer:

The crucial difference between git log and git reflog is that log shows the official branch history, while reflog shows the unofficial local history of how you got there.

To see it, run:

git reflog

4.2 Inspecting the Reflog

Let’s run a real example to illustrate.

git reflog

Output:

ebf7c5c (HEAD -> main, origin/main) HEAD@{0}: pull --rebase origin main (finish): returning to refs/heads/main
ebf7c5c (HEAD -> main, origin/main) HEAD@{1}: pull --rebase origin main (pick): main: modified readme - README.md
0535e79 HEAD@{2}: pull --rebase origin main (start): checkout 0535e79...
e7d0f19 HEAD@{3}: commit: main: modified readme - README.md
e5c4de0 HEAD@{4}: merge origin/main: Fast-forward
453b2c9 HEAD@{5}: commit: Rebasing Done with Updated Readme
...

Here’s how to interpret this output:

This is why reflog is often described as Git’s time machine: it allows you to travel back to states that no longer belong to the visible branch history.

4.3 Why Reflog Matters

Imagine you accidentally ran a destructive command:

git reset --hard bee45fc

Suddenly, several commits vanish from git log. At first glance, it appears they’re gone forever. But because the reflog logs every pointer movement, those commits still exist in the object database. They are simply no longer referenced by the branch.

git reflog
bee45fc (HEAD -> main) HEAD@{0}: reset: moving to bee45fc
ebf7c5c (origin/main) HEAD@{1}: reset: moving to ebf7c5c
bee45fc (HEAD -> main) HEAD@{2}: commit: main: deleted server info - config.yaml

Even though HEAD now points to bee45fc, you can still recover the abandoned commits by referencing their reflog entries.

4.4 Inspecting Past States

The reflog allows you to examine any past state using either numerical indices (HEAD@{n}) or time-based queries.

Numerical access:

git show HEAD@{29}

Output:

commit f86213de...
Author: Suraj Bhardwaj
Date: Thu Sep 11 17:45:43 2025 +0200

first commit

Time-based access:

git show main@{1.hour.ago}
git show main@{yesterday}
git show main@{1.week.ago}

Example:

git show main@{1.hour.ago}

Output:

commit e5c4de0...
Author: Suraj Bhardwaj
Date: Fri Sep 12 10:29:35 2025 +0200

Included Hyperparameter tuning

This ability to query the reflog by both sequence and time makes it extremely flexible for investigating past states.

4.5 Recovering Work with Reflog

The most common use case for reflog is recovering commits after an accidental reset, rebase, or amend.

Scenario 1: Hard Reset Gone Wrong

Suppose you reset backwards, thinking you no longer needed your latest commits:

git reset --hard bee45fc

After this, git log shows no trace of the lost commits. But the reflog does:

git reflog
bee45fc (HEAD -> main) HEAD@{0}: reset: moving to bee45fc
ebf7c5c (origin/main) HEAD@{1}: reset: moving to ebf7c5c

You can restore those commits by checking out an earlier state and branching from it:

git checkout HEAD@{1}
git checkout -b restore-branch
git checkout main
git merge restore-branch
git branch -d restore-branch

Your “lost” work is safely reintegrated into main.

Scenario 2: Rebase Misstep

Rebasing often requires resolving conflicts commit by commit. If you make a mistake during rebase and abort or reset incorrectly, commits can appear to vanish. Reflog preserves each step of the rebase (pick, squash, continue) so you can re-locate the original commits and restore them.

For example:

HEAD@{0}: rebase finished: returning to refs/heads/feature-branch
HEAD@{1}: rebase: commit message adjusted
HEAD@{2}: rebase: conflict resolved in 404.html

Even if the branch tip changes, each commit can be recovered via its reflog reference.

4.6 Where Reflog Data Lives

Technically, reflog entries are stored in .git/logs/. Every branch has its own log file, and there’s also a log for HEAD. Over time, very old entries may be cleaned up by Git’s garbage collection (git gc). This means reflog is not infinite/ the reflog is not permanent; it is a short- to medium-term recovery tool, not a permanent archive.

4.7 Reflog Cheat Sheet

Some essential commands to remember:

# Show full reflog of HEAD
git reflog
# Show reflog of a specific branch
git reflog show main
# Show commit at specific reflog index
git show HEAD@{5}
# Checkout an old state (detached)
git checkout HEAD@{10}
# Recover work by branching from reflog
git checkout -b restore-branch HEAD@{8}
# Query reflog by time
git show main@{yesterday}
git show main@{2.weeks.ago}
# Diff between states
git diff @{1.hour.ago}
# Show reflog in log format
git log -g

Section 4: Summary

The reflog is one of Git’s most powerful yet underutilized features. While git log shows the curated history of your project, the reflog records every pointer movement, allowing you to step back through time and recover work that appears lost.

Whenever you feel panic because “my commits are gone,” the reflog should be the very first place you look.

Conclusion

Mastering advanced Git commands is more than just memorizing syntax; it is about understanding how history, collaboration, and recovery mechanisms work together in a distributed version control system.

Together, these commands transform Git from a basic tool into a powerful system that gives you confidence to experiment, collaborate, and maintain clean project histories.

If you found this guide useful and want to follow more of my deep dives into software engineering, Git workflows, and AI/ML development practices, feel free to connect with me on LinkedIn: LinkedIn.

Do check out my GitHub repository for the more information on this guide: [GitHub Repository].