Git is more than a version control system. Once you move beyond the basics, it becomes a tool for managing complex histories, recovering from mistakes, and collaborating with multiple developers safely. This guide focuses on intermediate and professional Git usage. It explains not only commands but also why and when to use them, illustrated with real scenarios and diagrams.


1. Understanding Git Internals

Before diving into advanced workflows, it is crucial to understand what Git actually tracks. Many people think Git stores changes, but in reality, Git stores snapshots of your project at each commit.

Key Concepts

  • Commit: A snapshot of all files in your project. Each commit points to a parent commit, forming a chain.
  • HEAD: A pointer to the current branch tip. Your working copy is based on HEAD.
  • Branches: Lightweight pointers to commits. Moving a branch pointer does not alter commits.
  • Reflog: A history of where HEAD has pointed. This is your ultimate safety net for recovering lost work.

Scenario

You accidentally commit a new feature to the wrong branch. Instead of losing your work or manually copying files, you can move the commit using cherry-pick.

1
2
3
git checkout main
git cherry-pick D
git branch -d feature
1
2
3
          D (HEAD -> feature)
         /
A - B - C (main)

Commit D is on feature. Cherry-pick moves it safely to main.


2. Cleaning Up History

2.1 Interactive Rebase

Imagine you made five small commits for a single feature. Reviewers find the history messy. Interactive rebase lets you clean it up.

1
git rebase -i HEAD~5

An editor opens showing:

1
2
3
4
5
pick abc123 Add login function
pick def456 Fix login typo
pick ghi789 Add error handling
pick jkl012 Refactor login
pick mno345 Remove debug prints

You can change pick to squash to combine commits or drop to remove trivial commits. After saving, the branch will have a clean, concise history.

Scenario: You made several tiny commits while experimenting. Instead of showing all of them to your team, you squash them into a single meaningful commit:

1
Add login feature with error handling

This shows reviewers only the important changes.

1
2
3
4
5
Before Rebase:
A - B - C - D - E

After Rebase (squashed):
A - B - X

X combines D and E into one meaningful commit.


2.2 Commit Amend

If you forgot to add a file to your last commit, you can amend it without creating a new commit.

1
2
git add forgotten_file.py
git commit --amend

This updates the previous commit to include the new changes. Be careful if you already pushed, because this rewrites history.

Scenario: You committed login.py but forgot auth.py. Instead of making another commit, you amend to keep history clean.


2.3 Reflog

Reflog tracks all movements of HEAD, even commits that seem lost.

1
git reflog

Output example:

1
2
3
abc123 HEAD@{0}: reset: moving to HEAD~3
def456 HEAD@{1}: commit: added feature
ghi789 HEAD@{2}: commit: initial commit

Recover lost work:

1
git reset --hard def456

Scenario: You ran git reset --hard HEAD~3 by mistake. Reflog lets you go back to where you were.


3. Branching and Collaboration

3.1 Feature Branch Workflow

Branches are used to isolate work. Each developer can work on a feature without affecting main.

1
2
3
4
git checkout -b feature/login
# develop the login feature
git commit -m "Add login"
git push origin feature/login

When ready, merge into main:

1
2
git checkout main
git merge feature/login
1
2
3
4
main
|
|---- feature/login
|---- feature/payment

This keeps development organized and avoids conflicts.


3.2 Rebase vs Merge

If main advances while you work on a feature, you need to integrate changes.

Merge preserves the chronological history but adds a merge commit.

1
2
3
4
5
6
Merge:
A - B - C (main)
     \
      D - E (feature)
           \
            M (merge commit)

Rebase creates a linear history by moving your commits on top of main.

1
2
Rebase:
A - B - C - D' - E' (main)

Rule: Never rebase public shared branches. Use rebase for private branches before merging.


4. Recovery and Debugging

4.1 Git Bisect

When a bug appears but you are not sure which commit introduced it, Git bisect helps find it efficiently.

1
2
3
git bisect start
git bisect bad
git bisect good abc123

Git checks out the middle commit. You test and mark good or bad. Repeat until Git identifies the first bad commit.

Scenario: A regression appears in production. Bisect narrows down 500 commits to the single one that broke your code in only 9 steps.


4.2 Git Blame

Find out who last changed each line in a file:

1
git blame auth.py

This shows the author and commit for every line. It is useful for understanding why a line exists or for debugging.


4.3 Recover Deleted Branch

1
2
3
git reflog
# find commit hash
git checkout -b recovered <commit_hash>

This restores a deleted branch. Git never truly deletes commits until garbage collection, so recovery is usually possible.


5. Stash and Worktrees

5.1 Git Stash

When you need to switch branches but your work is not ready to commit:

1
2
3
4
5
6
git stash save "WIP login feature"
git switch main
# fix hotfix
git commit -m "Hotfix"
git switch feature/login
git stash pop

Scenario: You are midway through a feature when a production bug needs a quick fix. Stash lets you save your work temporarily.


5.2 Git Worktree

Sometimes you need two branches checked out at the same time.

1
git worktree add ../hotfix main

You now have a separate folder with an independent checkout of main for hotfixes.


5.3 Sparse Checkout

If you work in a large monorepo:

1
2
git sparse-checkout init
git sparse-checkout set frontend/

Only the frontend folder is checked out, saving disk space and time.


6. Cherry-Pick

Apply specific commits from another branch without merging the whole branch:

1
2
git checkout feature/login
git cherry-pick <commit_hash>

Scenario: A hotfix commit exists on main. Cherry-pick allows you to bring just that commit into your feature branch.


7. Hooks and Automation

Git hooks allow scripts to run automatically:

  • .git/hooks/pre-commit can run tests before a commit.
  • .git/hooks/pre-push can prevent pushing broken code.

Example:

1
2
#!/bin/sh
npm test || exit 1

This stops commits if tests fail.


8. Force Push Safely

Avoid overwriting teammates’ work. Instead of --force:

1
git push --force-with-lease

This ensures you only rewrite your own changes, not someone else’s.


9. Diff and Log Magic

1
2
3
git diff --word-diff           # shows changes at the word level
git diff main..feature         # compare branches
git log --graph --oneline --decorate --all  # visualize commit graph

Scenario: During code review, you can use --word-diff to highlight exact changes in lines instead of entire lines.


10. Daily Professional Workflow

  1. git fetch origin to update local view of remote branches.
  2. git rebase origin/main to keep your branch up to date.
  3. Work on feature branch and commit regularly.
  4. Rebase interactively before opening a PR.
  5. Push with --force-with-lease.
  6. Open PR and wait for code review.
  7. Merge after review and cleanup if necessary.

Closing Notes

Honestly, Git clicked for me only after I broke something badly enough to need reflog. That is how most people actually learn it. You read the docs, nod along, and then one day you do a reset --hard on the wrong branch and suddenly every command in this guide becomes very real and very urgent. The fear of losing work is what finally makes you curious about how Git actually stores things under the hood.

The commands here are not a checklist to memorize. Think of them as tools you reach for when a specific situation demands them. You will not use git bisect every week, but the one time a subtle bug sneaks into production and you have no idea which of the last eighty commits introduced it, you will be very glad you know it exists. Same with worktrees — it sounds niche until you are in the middle of a long refactor and a critical bug drops in your lap and you just want a clean checkout without disturbing what you have already built.

A few things that have genuinely helped me over time:

  • Write commit messages like you are explaining it to yourself six months from now. “Fix bug” is useless. “Fix null pointer in auth middleware when session token is missing” saves you a full context reload later.
  • Rebase before you raise a PR, every single time. A clean linear history makes reviewing faster and git log actually readable. Your teammates will notice, even if they never say it.
  • Do not be afraid of interactive rebase on your own branch. A lot of developers treat their local history as sacred. It is not. The point of rebase -i is to shape your work into something coherent before it becomes someone else’s problem to review.
  • --force-with-lease is not optional if you rebase. Just make it muscle memory. The regular --force is a footgun and there is no good reason to use it on a shared branch.
  • Treat reflog as your undo history. Git almost never deletes anything immediately. If something feels lost, check reflog before you start recreating work from scratch.

The real shift happens when you stop thinking of Git as a thing you do after writing code and start thinking of it as part of how you think about the work itself. Branching cheaply, committing often, cleaning up before sharing, these habits make you faster, not slower, because you stop being afraid of the codebase and start moving through it with confidence.