A practical guide to advanced Git workflows — rewriting history, rebasing, fetching, pulling with rebase, and using reflog to recover lost commits.
Git is more than add
, commit
, and push
. Once you start collaborating on real projects, you’ll often need to rewrite history, clean up commits, rebase branches, or even recover “lost” work. This guide explores advanced Git features with explanations and space for practical demonstrations.
Amending in Git is like fixing or updating your most recent commit without creating a brand-new one. Instead of stacking another commit on top, you “rewrite” the last one.
git log --oneline
0832375 (HEAD -> main, origin/main) HTML File
f86213d first commit
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
# Stage the changes
git add 404.html
git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 404.html
In other words, now we want to rewrite the previous commit SHA “ab40261” and amend it to reflect the changes made in both blog.html and 404.html file.
# Commit the changes with amend and no edit flag
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
# The commit has been made with new SHA and the old commit shaw is deleted
git log --oneline
4bf9f69 (HEAD -> main) main:modified the title - blog.html
0832375 (origin/main) HTML File
f86213d first commit
git add config.yaml
git commit --amend
[main 65cf379] main:modified the title - blog.html and added 404.html; added config.yaml
Date: Thu Sep 11 20:15:36 2025 +0200
4 files changed, 80 insertions(+), 1 deletion(-)
create mode 100644 404.html
create mode 100644 config.yaml
git log --oneline
65cf379 (HEAD -> main) main:modified the title - blog.html and added 404.html; added config.yaml
0832375 (origin/main) HTML File
f86213d first commit
If you only want to fix the commit message and not change the files, Git gives you a flag for that:
git commit --amend -m "New, corrected commit message"
That command replaces the old message with the new one while keeping the commit’s content exactly the same.
Alternatively, if you want to open your editor to edit the message interactively:
git commit --amend
Just don’t stage any new files before running it—otherwise Git will also include those changes in the amended commit.
Rebase moves your commits to a new base commit. Instead of merging two branches (which creates a merge commit), rebase replays your branch’s commits on top of another branch, producing a linear history.
# Create a new branch named feature-branch
git checkout -b feature-branch
# sample command for rebase
git rebase <BASE>
# base can be: an ID, a branch name, a tag, or a relative referance to HEAD
git rebase main
git log --oneline --decorate --graph --all
* 89293e4 (HEAD -> main, origin/main) Rebasing
* 86bcbc6 Updated Readme with future section
* 65cf379 main:modified the title - blog.html and added 404.html; added config.yaml
* 0832375 HTML File
* f86213d first commit
In this walkthrough, we will simulate a real scenario: creating a feature branch, making changes, modifying main
, and then rebasing with conflicts.
git checkout -b feature-branch
Output:
Switched to a new branch 'feature-branch'
At this point, both main
and feature-branch
point to the same commit.
git commit -am "feature-branch: modified title - 404.html"
[feature-branch 82ff182] feature-branch: modified title - 404.html
2 files changed, 21 insertions(+), 1 deletion(-)
git commit -am "feature-branch: modified Error Message - 404.html"
[feature-branch e7f8a2e] feature-branch: modified Error Message - 404.html
2 files changed, 8 insertions(+), 2 deletions(-)
Now, feature-branch
has two commits that are not present in main
.
Switch back to main
and simulate independent changes:
git checkout main
git commit -am "main: modified title again- 404.html"
[main 84900fe] main: modified title again- 404.html
1 file changed, 2 insertions(+), 2 deletions(-)
git commit -am "main: modified Error Message again- 404.html"
[main 9cbea38] main: modified Error Message again- 404.html
1 file changed, 2 insertions(+), 2 deletions(-)
Visual Log (git log --oneline --decorate --graph --all
):
* 9cbea38 (HEAD -> 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
|/
* 89293e4 (origin/main) Rebasing
* 86bcbc6 Updated Readme with future section
Now both branches have diverged.
git checkout feature-branch
Switched to branch 'feature-branch'
git rebase main
Output:
Auto-merging 404.html
CONFLICT (content): Merge conflict in 404.html
error: could not apply 82ff182... feature-branch: modified title - 404.html
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
Git stopped because of a conflict in 404.html
.
git mergetool
Output:
Merging:
404.html
Normal merge conflict for '404.html':
{local}: modified file
{remote}: modified file
Open the file, resolve the conflict, then stage the changes:
git add 404.html
git rebase --continue
Output:
[detached HEAD 352a722] feature-branch: Rebasing the feature branch - 404.html
2 files changed, 21 insertions(+), 1 deletion(-)
Auto-merging 404.html
CONFLICT (content): Merge conflict in 404.html
error: could not apply e7f8a2e... feature-branch: modified Error Message - 404.html
Another conflict occurred in the next commit.
Fix the file again, then run:
git add 404.html
git rebase --continue
Output:
[detached HEAD 24d3a81] feature-branch: rebase in progress - 404.html
2 files changed, 8 insertions(+), 2 deletions(-)
Successfully rebased and updated refs/heads/feature-branch.
All conflicts are resolved, and rebasing is complete.
git log --oneline --decorate --graph --all
Output:
* fca2f48 (HEAD -> feature-branch) feature-branch: modified README.md File - README.md
* 24d3a81 feature-branch: rebase in progress - 404.html
* 352a722 feature-branch: Rebasing the feature branch - 404.html
* 9cbea38 (main) main: modified Error Message again- 404.html
* 84900fe main: modified title again- 404.html
* 89293e4 (origin/main) Rebasing
Notice how feature-branch
commits are now on top of main
, forming a linear history.
Since feature-branch
is rebased, merging results in a fast-forward:
git checkout main
git merge feature-branch
Output:
Updating 9cbea38..fca2f48
Fast-forward
404.html | 4 ++--
README.md | 38 ++++++++++++++++++++++++++++++++++++++
main
now contains all rebased commits.
git branch -d feature-branch
Deleted branch feature-branch (was fca2f48).
Abort rebase and go back:
git rebase --abort
Skip a conflicting commit:
git rebase --skip
Continue after fixing conflicts:
git rebase --continue
main
to ensure history clarity.This simulation demonstrated a real-world rebasing scenario with conflicts, including how to resolve them and preserve a clean history.
When working with Git in collaborative projects, it’s important to keep your local repository aware of changes that have happened on the remote without immediately applying them. That’s where git fetch
comes in.
The command:
git fetch origin
origin
).origin/main
) to match the remote.main
).This makes git fetch
a safe operation—you can inspect remote changes before deciding whether to merge or rebase them into your local branch.
List all local and remote branches:
git branch -a
Output:
* main
remotes/origin/main
main
→ local branchremotes/origin/main
→ remote-tracking branch (your local copy of the remote branch)Check only local branches:
git branch -v
* main 453b2c9 Rebasing Done with Updated Readme
Check only remote branches:
git branch -r
origin/main
Check remote-tracking with last commit:
git branch -rv
origin/main 453b2c9 Rebasing Done with Updated Readme
See everything:
git branch -av
* main 453b2c9 Rebasing Done with Updated Readme
remotes/origin/main 453b2c9 Rebasing Done with Updated Readme
main
: local branch, currently checked out (*
).remotes/origin/main
: remote-tracking branch, representing the main
branch on origin
.Both point to the same commit 453b2c9
, meaning your local branch is in sync with remote.
Suppose new commits were added on the remote repository. Before fetching:
git log --oneline main
453b2c9 Rebasing Done with Updated Readme
... (older commits)
git log --oneline origin/main
453b2c9 Rebasing Done with Updated Readme
... (older commits)
Both look the same.
Now, after someone pushes two new commits (Included Hyperparameter tuning
, Created svm_classification.py
) to remote, your local copy is outdated.
Run:
git fetch origin main
Output:
From github.com:SurajBhar/advance_git
* branch main -> FETCH_HEAD
453b2c9..e5c4de0 main -> origin/main
Remote-tracking branch origin/main
is now updated, but local main
is untouched.
Check remote branch logs:
git log --oneline origin/main
e5c4de0 (origin/main) Included Hyperparameter tuning
86f6a84 Created svm_classification.py
453b2c9 Rebasing Done with Updated Readme
... (older commits)
Compare with local:
git status
On branch main
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
At this point:
origin/main
knows about the 2 new commits.main
is 2 commits behind.To bring your local branch up to date:
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:
git log --oneline --decorate --graph --all
* e5c4de0 (HEAD -> main, origin/main) Included Hyperparameter tuning
* 86f6a84 Created svm_classification.py
* 453b2c9 Rebasing Done with Updated Readme
...
Your local main
is synced with origin/main
.
git fetch
git fetch
alone does not update your local branch—you still need merge
or rebase
.origin/main
is updated but main
is not.git pull
(which does fetch + merge).git fetch
before starting your workday.git log origin/main
to review remote commits.git fetch --prune
cleans up deleted remote branches.git fetch
updates your view of the remote repository.When collaborating with others, keeping your local repository in sync with the remote repository is crucial. After all, you’re not the only one pushing changes!
git fetch
brings your remote-tracking branches (like origin/main
) up to date but does not touch your local working branch.git pull
goes further: it fetches changes and merges them into your local branch.In other words:
git pull = git fetch + git merge
Let’s say you are on main
and new commits have appeared on the remote repository. Running:
git pull origin main
origin/main
.main
.If there are conflicts, Git will stop and ask you to resolve them.
Sometimes merging is not the cleanest solution. If you are working on local changes but also want to integrate the latest updates from remote:
The command:
git pull --rebase origin main
works like this:
git fetch
→ gets latest changes from origin/main
.git rebase
→ temporarily “removes” your local commits, applies the new commits from origin/main
, then replays your local commits on top.Before pulling, the commit history looks like this:
git log --oneline --decorate --graph --all
* e5c4de0 (HEAD -> main, origin/main) Included Hyperparameter tuning
* 86f6a84 Created svm_classification.py
* 453b2c9 Rebasing Done with Updated Readme
* fca2f48 feature-branch:modified README.md File - README.md
* 24d3a81 feature-branch: rebase in progress - 404.html
* 352a722 feature-branch: Rebasing the feature branch - 404.html
* 9cbea38 main: modified Error Message again- 404.html
* 84900fe main: modified title again- 404.html
...
Two new commits (Update 2 config.yaml
, Updated 1 config.yaml
) exist on the remote but are not yet on local.
Now 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.
Check the new commit history:
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 the difference:
d9b0764
, 0535e79
) were applied first.ebf7c5c main: modified readme
) was rebased on top.git pull --rebase
git log
looks tidy).git pull --rebase
for personal feature branches or small sets of local commits.git config --global pull.rebase true
git rebase --continue
git rebase --abort
git pull
= fetch + merge → quick but may clutter history.git pull --rebase
= fetch + rebase → cleaner history, local commits reapplied on top of remote.Git’s reference logs (reflog) act as a safety net for your repository. While git log
shows the commits that are part of your branch’s history, git reflog
records all changes to the tips of branches and other references—even if those commits don’t appear in the visible history anymore.
Think of the reflog as a personal diary of your Git actions, maintained locally. Every time you:
…the HEAD
reference is updated, and the reflog records it.
Run:
git reflog
Example 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 what’s happening:
HEAD@{0}
– The most recent action. In this case, completing a pull with rebase.HEAD@{3}
– A commit made earlier (main: modified readme
).HEAD@{5}
– A commit you thought might be “lost” if you reset later.Unlike git log
, these entries remain visible even after resets or rebases.
Let’s say you reset a branch or made changes you regret.
git log
may no longer show those commits (they are detached from the branch).That’s why reflog is Git’s time machine.
You can inspect any past state of the repository with:
git show HEAD@{n}
Example:
git show HEAD@{29}
Output shows the very first commit:
commit f86213de...
Author: Suraj Bhardwaj
Date: Thu Sep 11 17:45:43 2025 +0200
first commit
You can also query reflog by time references:
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
Suppose you ran:
git reset --hard bee45fc
This moved your branch pointer back to an older commit. At first glance, it looks like you lost your newer commits. But reflog recorded them:
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
...
You can still recover the commits via their reflog entries.
You decide those commits weren’t mistakes after all. To bring them back:
# Checkout an old reflog entry in detached HEAD state
git checkout HEAD@{2}
# Create a new branch to save those commits
git checkout -b restore-branch
# Merge back into main
git checkout main
git merge restore-branch
# Delete the temporary branch
git branch -d restore-branch
Now your “lost” commits are restored into your main history.
.git/logs/
git gc
) may eventually clean up very old reflog entries.Here’s a quick summary of useful reflog commands:
# Show reflog for HEAD (all actions)
git reflog
# Show reflog for a specific branch
git reflog show main
# Show commit at a specific reflog entry
git show HEAD@{5}
# Checkout an old state
git checkout HEAD@{10}
# Recover work by branching from a reflog entry
git checkout -b restore-branch HEAD@{8}
# Show history by time references
git show main@{1.hour.ago}
git show main@{yesterday}
# Diff between old state and current
git diff @{1.hour.ago}
# Show reflog in log format
git log -g
In real-world projects, Git is more than just a tool for saving code — it’s the foundation of teamwork and clean collaboration.
The commands we covered — amending commits, rebasing, fetching, pulling with rebase, and using reflog — are the safety nets and precision tools that keep your history tidy and your team in sync. They also give you confidence that no mistake is truly permanent, since Git always keeps a trace.
As an AI/ML Engineer, I’ve seen how messy histories and merge chaos can slow teams down. Mastering these advanced commands makes a big difference: your commits stay meaningful, your branches merge cleanly, and your teammates trust the process more.
Next time you’re unsure about a Git situation, take a step back:
Keep practicing these on side projects — they’ll become second nature. And once they do, you’ll find that Git feels less like a source of stress and more like a true partner in your development workflow.
In the next post, I’ll explore Git Stash — a lifesaver for juggling multiple tasks in real-world AI/ML projects.
You can also check out the official Git documentation for deeper details.