· 16 min read

The Only Git Article You'd Ever Need - 4/4

Advanced Git Management

If you actually thoroughly read the first 3 parts of Git articles and willingly came here, you’re doing really good <3 Beyond basic version control, these advanced techniques enable easier project management, history manipulation, and disaster recovery. These tools distinguish senior developers who can handle complex Git scenarios from those who panic when shit hits the fan

Rewriting History: rebase vs. merge

This is one of Git’s most important debates. Both rebase and merge integrate changes from one branch into another, but they use different approaches to handling project history. Understanding this difference is pretty important for maintaining clean project timelines.

The Analogy: Historical Chronicle vs. Edited Biography

  • Merging (git merge): Think of merge as maintaining proper history. When you merge branches, you create a permanent record that shows exactly when and how different lines of development came together. This creates a “merge commit”, which is a specific marker that documents the integration of two development paths. The resulting history preserves the complete, authentic timeline of how the project actually got developed, including all the parallel work and merge points.

  • Rebasing (git rebase): Rebase is basically cleaner, edited history. Instead of showing the messiness of parallel development, rebase rewrites your commits as if they had always been based on the latest version of the target branch. The result is a linear, polished history that’s easier to follow but doesn’t reflect the actual chronological development process.

git merge <branch>: Preserving History (Messy and All)

  • What it does: Integrates changes from <branch> into your current branch by creating a new merge commit. This commit explicitly records that a merge happened, and it has two parent commits (the head of your branch and the head of the merged branch).
  • When to use:
    • Integrating features into main: This is the standard for bringing completed work into a shared, long-lived branch like main. It preserves the exact historical record of when the work was combined.
    • Collaborative branches: If you’re working on a feature branch with other people, merging main into your feature branch periodically (to keep it updated) is safer than rebasing, as it avoids rewriting history they might have already pulled.
  • Pros:
    • Preserves exact history: You can always see precisely when divergent lines of development were joined.
    • Safer on shared branches: Doesn’t rewrite history, so it won’t break other people’s work if they’ve already pulled your branch.
  • Cons:
    • Can lead to a messy history with many merge commits, especially if you’re frequently merging. This creates a “diamond shape” in your git log --graph.

git rebase <branch>: Rewriting History (for a Clean Look)

  • What it does: Takes your local commits on your current branch, temporarily sets them aside, moves your branch’s base to the latest commit of the specified <branch> (e.g., main), and then “replays” your original commits one by one on top of the new base. The result is a history that looks like your branch was always based on the latest version of <branch>.
  • When to use:
    • Cleaning up your local feature branch before a PR: This is the primary use case. Before you submit your useless new feature for review, rebase it onto the latest main. This makes your PR a single, linear set of changes that’s easy for reviewers to follow.
    • Squashing commits: You can interactively rebase (git rebase -i) to combine multiple small, messy commits into a single, clean one before pushing.
  • Pros:
    • Clean, linear history: Makes your git log beautiful and easy to read. No extraneous merge commits.
    • Easier to revert: Reverting a linear history is often simpler than reverting a history with many merges.
  • Cons:
    • REWRITES HISTORY: This is the big one. If you rebase a branch that someone else has already pulled and based their work on, you effectively change the “past” that they know. When they try to push or pull, Git will get confused, and they’ll experience a special kind of hell involving forced pushes and desperate pleas for help
    • There can, obviously, be conflicts during the replaying process, but that kind of goes for both rebase and merge.

The Golden Rule of Rebasing

  • NEVER EVER rebase a branch that has been shared with other people.
  • If your branch exists on origin (GitHub/GitLab) and someone else might have pulled it, do NOT rebase it. Use git merge instead. Rebasing is for cleaning up your own, local, private work-in-progress branches before you push them or open a PR. Break this rule, and you’ll be hated on for the rest of your life and get 7 men sent to jump you. Just don’t be that guy.

The “Oh Shit” Buttons: reset, revert, reflog

You will screw up. You will make bad commits. You will delete things you shouldn’t. These commands are your emergency tools. Learn them.

git reset <commit>

This is the command that moves your branch pointer backward in history. It can erase commits from your branch’s history. It’s powerful, dangerous. It’s very easy for things to go wrong here if you mess up.

  • What it does: It literally moves your current branch’s HEAD pointer to a specified commit. Anything after that commit is no longer considered part of that branch’s history.

  • Modes: This is where it gets tricky, and where you can lose work if you’re not careful.

    • --soft: Moves the branch pointer, but keeps all changes from the “erased” commits in your staging area. You can then commit them again as a new single commit, for example.

      git reset --soft HEAD~1 # uncommits the last commit, changes remain staged
      
    • --mixed (Default): Moves the branch pointer, keeps all changes from the “erased” commits in your working directory (unstaged).

      git reset HEAD~1 # Uncommits the last commit, changes are unstaged
      
    • --hard: Moves the branch pointer and discards all changes from the “erased” commits from both your staging area and your working directory. This is how you lose work. Your files will literally revert to the state of the target commit.

      git reset --hard HEAD~1 # Deletes the last commit and its changes from your local files
      
  • When to use:

    • To un-do recent local commits before you’ve pushed them.
    • To combine multiple local commits into one (often used with git rebase -i or git reset --soft).
    • To completely discard changes from your working directory (with --hard), but ensure you know what you’re doing.
  • NEVER use git reset --hard on a pushed commit on a shared branch. You will rewrite history and cause problems for anyone who has that commit.

git revert <commit>

This is the preferred way to undo changes on shared branches or in published history. It’s transparent and doesn’t rewrite history.

  • What it does: Instead of deleting the “bad” commit, git revert creates a new commit that does the exact opposite of the specified bad commit. If the bad commit added a line, the revert commit removes it. If it removed a line, the revert commit adds it back.
  • Pros:
    • Doesn’t rewrite history: Safe to use on shared or main branches because it creates a new commit, preserving the integrity of the existing history.
    • Transparent: The revert action itself is recorded in the history, making it clear that a change was undone.
  • Cons:
    • Shit gets messy. Imagine reverting a commit, which was already a revert of a revert…you get the point.
  • When to use:
    • To undo a commit without destroying history.

git reflog

Even if you use git reset --hard and think you’ve lost your work, Git is probably still tracking it. reflog is your secret weapon.

  • What it does: Git keeps a local log of almost every single place your HEAD pointer has been. Every time you checkout a branch, commit, rebase, reset, or stash, an entry is added to the reflog. It’s a chronological history of your local Git activity.

  • Usage:

    git reflog
    

    Example Output:

    f1d3a5b (HEAD -> main) HEAD@{0}: commit: feat: Add Tower 2
    a0b1c2d HEAD@{1}: merge feature/new-thing: Fast-forward
    1234567 HEAD@{2}: checkout: moving from feature/new-thing to main
    a1b2c3d HEAD@{3}: commit: feat: Add Tower 1
    b2c3d4e HEAD@{4}: checkout: moving from main to feature/new-thing
    ... (many more entries)
    

    Each entry has a hash and an HEAD@{N} index.

  • How to use it to save your ass: If you accidentally git reset --hard and wiped out a bunch of commits, you can find the commit hash before your screw-up in the reflog and then git reset --hard <that_hash> to restore your branch to that point.

    # Suppose HEAD@{3} was the commit you accidentally lost
    git reset --hard HEAD@{3}
    # Or
    git reset --hard a1b2c3d
    

    This is your literal undo button for Git operations. A life saver in some scenarios.

Monorepo vs. Multi-repo

This isn’t just Git; it’s a fundamental architectural decision for how a company organizes its code. Even if the place you work at does not let you choose this, understanding the trade-offs will make you sound smart in an interview (or at least less dumb).

Monorepo: One Repository for Everything

  • Concept: A single, colossal Git repository contains the code for all/multiple projects, services, libraries, and applications developed by a company. Think of it as one massive shared drive where everyone dumps their code, but with Git shit on top.
  • Pros:
    • Easy Sharing: A single place to find all code. Easier to reuse components or libraries across projects.
    • Atomic Cross-Project Changes: You can make a single commit that updates a shared library and all the applications that use it. This ensures consistency and avoids versioning hell between services.
    • Simplified Dependency Management: Common dependencies can be managed once at the root.
    • Refactoring: Easier to refactor code across multiple services simultaneously.
  • Cons:
    • Can Get Slow: The repository can become absolutely massive (hundreds of GBs). Cloning, fetching, and even git status can be agonizingly slow without specialized tooling.
    • Complex Build Tooling Required: Building a monorepo efficiently requires sophisticated build systems (like Bazel, Nx) that only build what’s changed and manage dependencies across hundreds of projects.
    • Impact of Bad Commits: A single bad commit can affect many projects, literally leading widespread build failures.

Multi-repo: A Repository for Every Project

  • Concept: Each project, microservice, or distinct library gets its own dedicated Git repository.
  • Pros:
    • Clear Ownership: Each repo has a clear team or individual responsible for it.
    • Independent Release Cycles: Teams can deploy their services independently without affecting others.
    • Simple Build Processes Per Repo: Builds are generally faster as they only focus on a single, smaller codebase.
    • Easier Access Control: Granular permissions can be set per repository.
  • Cons:
    • Code Discovery is a Nightmare: Finding reusable code means searching across dozens or hundreds of separate repositories.
    • Versioning Hell: If Project A uses Library L v1.0, and Project B wants to use L v2.0, you might end up with conflicting versions, or coordinating updates becomes a massive pain across many repos.
    • Coordinating Changes: A single logical change (e.g., updating a common API client) might require multiple PRs across multiple repositories, each needing review and deployment coordination.
    • Inconsistent Tooling/Practices: Different repos might adopt different coding styles, linters, or CI setups, leading to fragmentation. Obviously, this can be fixed but would require some extra effort and time

Important Extras (How to Tell Others to Git Gud)

These commands are your secret weapons. They’re for those moments when standard Git operations just don’t cut it. Devs around you who taught themselves from youtubers won’t have a clue about what they can do in these situations. You will. Be better.

git stash: The Junk Drawer

Imagine you’re balls deep in a feature, your working directory is a chaotic mess of half-written code, untested changes, and commented-out debugging lines. Suddenly, a critical bug report comes in from production. You have to switch branches to fix it, but you can’t commit your current work because it’s broken, and you don’t want to lose your progress.

Enter git stash. This is your junk drawer for such changes. Git sweeps all your modifications (staged and unstaged) into a temporary holding area, cleaning your working directory. Your branch immediately reverts to its last committed state. You go fix the urgent bug, come back, and then git stash pop to retrieve all your messy changes right back where you left them.

  • How it works: Git creates a new commit object (or two, if you have staged and unstaged changes) that represents the state of your working directory and adds it to a special “stash” reference. It then resets your current branch to HEAD, effectively undoing your uncommitted changes.

  • Usage:

    • git stash save "WIP: Feature X" (or just git stash for a default message): Takes all modified tracked files and stages them, then resets your working directory to HEAD. The optional message helps you remember what’s in the stash.

      # You're on 'feature/auth', code is messy
      git stash save "WIP: Auth forms redesign"
      # Output: "Saved working directory and index state WIP on feature/auth: ..."
      # Your directory is now clean.
      
    • git stash list: Shows you all your stashed changes.

      git stash list
      # Output:
      # stash@{0}: WIP on feature/auth: Auth forms redesign
      # stash@{1}: On bugfix/typo: Initial typo fix (older stash)
      
    • git stash pop: Applies the most recently stashed changes back to your working directory and removes them from the stash list.

      # You're back on 'feature/auth'
      git stash pop
      # Output: "On branch feature/auth ... Dropped refs/stash@{0}..."
      # Your messy changes are back.
      
    • git stash apply: Applies the stashed changes but keeps them in the stash list. Useful if you want to apply the same stash to multiple branches.

    • git stash drop: Deletes a specific stash from the list.

  • When to use: When you need a clean working directory now, but don’t want to commit incomplete or broken work. It’s for context switching without committing garbage.

git cherry-pick <commit-hash>

Sometimes, you don’t want to merge an entire branch. You just need one specific commit from another branch, and you need it right here, right now, on your current branch. Maybe it’s a critical hotfix that was developed on a separate feature branch, or a single brilliant optimization buried deep in someone else’s experimental work. git cherry-pick is your scalpel.

  • How it works: Git takes the changes introduced by a specific commit (identified by its hash) from one branch and attempts to apply them as a new commit on your currently checked-out branch. It’s like copying and pasting a single change set.

  • Usage:

    1. Find the hash of the commit you want to cherry-pick (use git log --oneline on the source branch).

    2. Switch to the branch where you want to apply the commit.

    3. Run git cherry-pick <commit-hash>.

      # Let's say you're on 'bugfix/critical-prod'
      # And you see a crucial commit on 'feature/devops-magic':
      #   abc1234 feat: Add robust error logging
      # You want only that commit.
      
      git checkout bugfix/critical-prod
      git cherry-pick abc1234
      # Git will apply the changes and create a *new* commit on your current branch with the same message.
      # Conflicts can happen during cherry-picking, just like with merges. Resolve them and 'git add' then 'git cherry-pick --continue'.
      
  • When to use: For applying individual bug fixes, small features, or optimizations across branches without merging entire lines of development

  • Buttttt cherry-picking can lead to duplicated commits (same content, different hashes) in your history if you later merge the original branch. Use it carefully.

git bisect

You’ve got a bug. It wasn’t there last week, but it’s here today. Or this morning. Or sometime in the last 500 commits, and you have no idea where it came from. Manually checking out commits and testing is a nightmare. git bisect automates this painful process using a binary search algorithm (it’s okay if you don’t know what this is yet!), cutting your search space in half with each step. It’s so simple yet so helpful.

  • How it works:

    1. You tell git bisect a “good” commit (where the bug didn’t exist) and a “bad” commit (where the bug does exist, usually HEAD).
    2. Git then checks out a commit roughly in the middle of that range.
    3. You test the code at that commit and tell Git: “This is good” or “This is bad.”
    4. Git, based on your feedback, halves the remaining search range and checks out a new commit.
    5. It repeats this process until it pinpoints the single commit that introduced the bug.
  • Usage:

    1. Start bisecting:

      git bisect start
      
    2. Mark the current (buggy) commit as “bad”:

      git bisect bad
      
    3. Find a “good” (working) commit: Look at your git log and find a commit hash from before the bug was introduced. Mark it as “good.”

      git bisect good <commit-hash-from-before-bug>
      
    4. Git will now start checking out commits in the middle of the range.

      Bisecting: N revisions left to test, ~X steps left.
      [some_commit_hash] Message for this commit
      
    5. Test the code: Run your tests (manual or automated) on the currently checked-out commit.

    6. Tell Git the result:

      • If the bug is present: git bisect bad
      • If the bug is not present: git bisect good
    7. Repeat steps 5-6 until Git finds the culprit.

      [culprit_commit_hash] This is the commit that introduced the bug.
      author: John Doe
      date: ...
      
    8. Clean up: After finding the bug, always return to your original branch:

      git bisect reset
      
  • When to use: When you’ve got a a bug that reappeared or was introduced and you need to pinpoint the exact commit responsible without manually going through hundreds of changes. It’s an absolute lifesaver for bug hunts.

It’s All…Pointers? Always Has Been

Many developers treat Git as a black box. They know git commit does “something” and git push sends it “somewhere.” But truly understanding Git involves a fundamental realization: it’s not magic. It’s just files pointing to other files.

  • Pointers (References or “Refs”): In Git, branches (main, feature/X), tags (v1.0.0), and even special pointers like HEAD are nothing more than simple text files that contain the SHA-1 hash of a commit object. They are literally pointers.
    • Branches: When you create a branch, Git creates a file in .git/refs/heads/ with the branch’s name. Inside that file is the hash of the commit it currently points to. When you git commit, this file’s content (the hash) is updated to point to your new commit.
    • Tags: Similar to branches, but they’re typically static pointers that don’t move. A tag v1.0.0 points to a specific commit and stays there. (Files in .git/refs/tags/)
  • HEAD: This is the most important “ref.” It’s not a branch itself; it’s a symbolic reference that points to where you currently are in your repository.
    • If you’re on a branch (e.g., main), HEAD points to the main ref. When you commit, main moves forward, and HEAD moves with it.
    • If you git checkout <commit-hash>, you’re in a “detached HEAD” state. HEAD is directly pointing to a commit hash, not a branch ref. This means if you commit, HEAD will move, but no branch ref will, making your new commits unreachable unless you create a new branch.
  • Demystification: Go into a .git folder (but don’t change anything unless you just wanna mess with stuff to learn/understand). You’ll find directories like refs/heads/ and refs/tags/. Open a file in refs/heads/ (e.g., main). You’ll see a commit hash. That’s it. That’s your branch. It’s just a file that says “the main branch is currently at this commit.”

This understanding transforms Git from a mysterious black box into a logical system of linked data. When you git checkout, Git isn’t performing magic; it’s just changing where HEAD points and updating your working directory to match the commit that HEAD now points to. Knowing this helps you predict how Git will behave and makes debugging weird issues much simpler.


You’ve now mastered Git from fundamental concepts to advanced techniques. This balls deep understanding puts you ahead of most developers who only know basic commands without understanding the actual thing making this all work.

The real value comes from applying this knowledge consistently in your daily development work. Focus on building good habits: meaningful commit messages, proper branching strategies, and clean merge practices. These fundamentals will serve you throughout your entire development career.

You don’t have to memorize every command, just understand the principles that make version control effective for both solo work and collaboration. With this foundation, you can tackle any Git challenge with confidence.

Keep trying lil bro, you’re being re-built piece by piece into an absolute beast, love to see that

See you around, smarter stranger :)