Sitemap

Rebase, Merge, and the Philosophy of History in Git

Should history be preserved in all its messy detail, or rewritten into a clean and seamless narrative?

14 min readAug 30, 2025

--

Introduction

Every act of programming is, in a sense, an inscription in time. A line of code is not only an instruction to a machine; it is also a trace of human thought, a decision fixed into a record. Each commit in Git becomes a moment captured, a crystallization of intent. Branches stretch out like parallel timelines, each one representing an alternative path, a set of possibilities not yet reconciled with the whole. And when those paths converge, when branches meet, we are confronted with a question that is as philosophical as it is technical: how should history be written?

Historians have always wrestled with this dilemma. Do we record events exactly as they happened, with all their contradictions, accidents, and scars? Or do we retell the story as a coherent narrative, stripping away detours and distractions so that it reads as if it flowed naturally from beginning to end? One approach preserves authenticity; the other provides clarity.

Git forces developers into the same tension. A merge preserves the story as it truly unfolded: two branches diverged, each saw work done, and then they met again. Nothing is erased, nothing concealed. A rebase, by contrast, rewrites the narrative so that your branch appears as if it had always grown directly from the trunk of history. Both are valid, both are powerful, and both reveal something profound about how we think about collaboration, memory, and truth.

This is why Git is more than a version control system. It is a philosophy of time. With every decision, whether to merge or to rebase, we decide whether our code will carry the marks of its struggles, or whether it will be polished into a story that feels seamless and inevitable. In Git, just as in life, the past is not merely recorded; it is curated.

How Does Git Work ?

Beneath the commands and the intimidating jargon, Git is not magic. It is mathematics. At its core lies a structure that computer scientists know well: the directed acyclic graph, or DAG. This is the geometry of time as Git understands it.

Each commit in Git is a node in this graph. A node does not exist in isolation, it points back to one or more parents. The edges that connect nodes always move forward, never backward, which is why the graph is “acyclic.” Once a commit has been made, it cannot point back and rewrite its own ancestry. In this sense, Git encodes the irreversibility of time.

Branches are nothing more than labels, pointers that mark positions in this graph. When you create a new branch, you are not duplicating history. You are simply placing a bookmark in the DAG, a reminder that “from here, another line of development began.”

You can see this graph-like structure with just a few commands:

# Start a new repository
git init history-demo
cd history-demo

# First commit
echo "Hello" > file.txt
git add file.txt
git commit -m "Initial commit"

# Create a new branch
git checkout -b feature
echo "New feature" >> file.txt
git commit -am "Add feature"

# Switch back to main and diverge
git checkout main
echo "Main update" >> file.txt
git commit -am "Update from main"

Now, run:

git log --oneline --graph --all

You’ll see something like this:

* 3a1c2f4 (main) Update from main
| * 9b8d7e6 (feature) Add feature
|/
* 1a2b3c4 Initial commit

This is Git showing you the DAG: one commit that diverged into two lines of history, with main and feature each moving forward independently.

Merging introduces a special kind of node, one with multiple parents. If you now merge feature back into main:

git checkout main
git merge feature
git log --oneline --graph --all

You will see the DAG expand again:

*   4d5e6f7 (HEAD -> main) Merge branch 'feature'
|\
| * 9b8d7e6 (feature) Add feature
* | 3a1c2f4 Update from main
|/
* 1a2b3c4 Initial commit

The merge commit (4d5e6f7) ties the two stories together, recording the fact that they diverged and then converged.

This scientific framing explains why Git feels at once liberating and disorienting. Older version control systems such as Subversion or CVS enforced a single straight line of history, a railroad track where every commit simply followed the one before it. Git shattered that model. By representing history as a graph, it allowed parallel timelines, divergent experiments, and multiple perspectives. But with that freedom came the burden of choice: do we keep the branching structure as it is, honoring every fork and join, or do we flatten it into something linear for the sake of readability?

The genius of Git is that it exposes developers to this tension directly. It shows you the raw DAG, the true record of causality. But it also gives you tools like merge and rebase to reshape it. Git is not just a system for recording history; it is a system for editing history. The mathematics of the DAG provides the foundation, but the philosophy of history is left in your hands.

Rebase: The Story You Wish You Had Written

If merging records events as they happened, rebasing is closer to editing a manuscript. Imagine writing a diary where the entries are scattered, out of order, filled with half-finished thoughts. Rebasing is like taking those notes, rewriting them neatly, and placing them in sequence so that the story flows as if it had always been coherent. It does not change the content of your work, but it alters the narrative through which that work is remembered.

Technically, git rebase works by lifting commits from one ancestry and replaying them on top of a new base. Instead of creating a merge node that acknowledges multiple parents, rebase redraws the edges of the DAG so that it looks as if your branch had always sprouted from the latest point on main. Let’s see this in action:

# Start a new repository
git init rebase-demo
cd rebase-demo

# First commit on main
echo "line1" > file.txt
git add file.txt
git commit -m "Initial commit"

# Create a feature branch and add a commit
git checkout -b feature
echo "feature work" >> file.txt
git commit -am "Add feature work"

# Meanwhile, main moves forward
git checkout main
echo "main update" >> file.txt
git commit -am "Main update"

# Rebase feature onto the latest main
git checkout feature
git rebase main

Now, check the history:

git log --oneline --graph --all

You will see a straight line of commits, no branching:

* 7e9f3a2 (feature) Add feature work
* 2c1b6d5 (main) Main update
* 1a2b3c4 Initial commit

The feature branch has been replayed on top of main. It looks as though you had written “Add feature work” after “Main update,” even though in reality the commits were created independently.

This is the magic, and the danger, of rebase. It produces a clean, linear history that reads effortlessly. Tools like git blame or git bisect benefit from such linearity, since they no longer have to navigate the detours of merges. A rebased branch feels like a polished story rather than a messy diary.

But rebasing also rewrites history. The commit 7e9f3a2 is not the same as the original feature commit before rebasing, it is a new commit with the same content but a different ancestry. If you had already shared the original branch with collaborators, their history will no longer match yours. This creates a parallel universe: two timelines that look similar but are incompatible.

For this reason, rebasing is best suited for private work: cleaning up a branch before opening a pull request, or tidying a sequence of small, fragmented commits into a coherent whole. Used this way, it is a gift to your collaborators, sparing them the messiness of your draft. But used recklessly, on branches that are already public, it becomes revisionism of the most destructive kind, erasing shared history and replacing it with a counterfeit.

Philosophically, then, rebasing is not the historian’s record but the editor’s revision. It is the story you wish you had written. And like all revisions, it can illuminate or it can distort, depending on the hands that wield it.

Merge: The Story As It Happened

If rebasing is revision, merging is honesty. It is the refusal to rewrite what happened, even if the story is messy. A merge commit says: two branches existed, they diverged, they each carried their own work, and now they meet again. Nothing is hidden, nothing is retold. The scars remain visible in the graph, but they are true scars.

In Git, a merge commit is unique: it has more than one parent. Unlike a rebase, which replays commits onto a new base, a merge simply acknowledges that multiple ancestries now converge. It creates a snapshot of the project at the moment of union, while preserving the full record of how each branch reached that point.

Let’s look at this in practice. Begin with the same divergence we created earlier:

# Start fresh
git init merge-demo
cd merge-demo

# First commit on main
echo "line1" > file.txt
git add file.txt
git commit -m "Initial commit"

# Create a feature branch and add work
git checkout -b feature
echo "feature work" >> file.txt
git commit -am "Add feature work"

# Meanwhile, main moves forward
git checkout main
echo "main update" >> file.txt
git commit -am "Main update"

At this point, the history looks like two paths diverging:

git log --oneline --graph --all
* 7e9f3a2 (main) Main update
| * 5c2d1f9 (feature) Add feature work
|/
* 1a2b3c4 Initial commit

Now, instead of rebasing, merge the two branches:

git merge feature

Git creates a new commit with two parents:

*   8f4a1d0 (HEAD -> main) Merge branch 'feature'
|\
| * 5c2d1f9 (feature) Add feature work
* | 7e9f3a2 Main update
|/
* 1a2b3c4 Initial commit

The graph is no longer a straight line. It is a diary entry with all the interruptions preserved. You can see that main and feature both diverged from the initial commit, each advanced on its own, and then joined back together. The merge commit 8f4a1d0 marks the reconciliation, preserving both ancestries.

This honesty comes at a cost. If your team merges often, your history will fill with merge commits. The log may become dense, tangled, intimidating for newcomers. Rebasing could have polished this into a neat linear path, but merge insists on fidelity.

And yet, that fidelity can be invaluable. Merge commits are landmarks: they show precisely when branches converged, when code from multiple contributors came together, when integration occurred. In large projects, this record is crucial for debugging. If a regression was introduced, you may not only ask which commit caused it, but which integration introduced it. Without merge commits, that story disappears.

Philosophically, merge is the diary of development: unfiltered, contradictory, sometimes confusing, but authentic. It preserves the human reality of collaboration, the moments where work split apart, the negotiations required to bring it back together. If rebase is the polished memoir, merge is the raw diary, and both tell truths of a different kind.

Two Cultures of History

The abstract debate between merge and rebase becomes vivid when we step into real projects. Different communities have made very different choices about how their history should be written, and these choices reveal their values as much as their workflows.

Consider the Linux kernel, one of the largest and longest-running collaborative software projects in history. Thousands of developers contribute worldwide, and its code is stewarded by a hierarchy of maintainers. In such a vast ecosystem, authenticity of history is essential. Maintainers rely heavily on merge commits. They want to know not just what was added, but when and how different streams of work converged. Each merge is a historical marker, a way of auditing integration. The Linux graph is messy, sprawling, almost intimidating, but it is also a faithful record of distributed collaboration at scale. In this culture, merge commits are not noise; they are the lifeblood of traceability.

Now step into the world of startups. A small team of ten engineers, moving fast, trying to ship features to market before their competitors. In this environment, clarity often matters more than historical fidelity. Onboarding a new engineer should not require deciphering a forest of merge commits. A rebased history, linear, neat, with commits that read like chapters in a book, is far easier to follow. Rebasing is used liberally: developers are expected to clean their feature branches before opening pull requests, often squashing small experiments into a handful of meaningful commits. The resulting history is curated, a narrative designed for readability rather than for forensic reconstruction.

These two cultures, the merge-heavy cathedral of Linux and the rebase-first garage of a startup, illustrate how Git strategies are never just technical. They are cultural. They are reflections of scale, of priorities, of how teams perceive their own work. One values authenticity and auditability; the other values clarity and velocity.

The point is not that one is better than the other. Rather, Git gives us the freedom to choose. With each decision, to merge or to rebase, we are not only shaping code history but also articulating what kind of community we want to be.

Beyond Merge and Rebase

To think of Git as offering only merge and rebase is to underestimate its subtlety. These are the two great philosophies of history, authenticity versus curation, but Git also provides other instruments, each reflecting a different way of remembering, forgetting, or correcting. Together they form a toolbox for shaping narrative, for deciding what is preserved and what is hidden.

Cherry-pick: Selective Memory

Cherry-picking is the act of remembering only what you want. It lets you take a single commit from one branch and apply it to another, as if saying, “I value this part of history, but not the whole story.”

# Suppose a fix was made on a feature branch
git checkout feature
echo "critical fix" >> file.txt
git commit -am "Fix bug in file"

# Apply just that fix to main without merging everything else
git checkout main
git cherry-pick <commit_sha>

This is invaluable for hotfixes: a security patch developed on a feature branch can be applied immediately to main without dragging in unfinished work. But philosophically, cherry-pick fragments history. The same change now exists in two places, making the story harder to follow. It is memory, but selective memory.

Squash: Curation

Squashing commits is curation at its most deliberate. Instead of preserving every small edit, typo fix, and “oops” commit, you collapse them into a single, coherent story.

# Interactive rebase with squash
git rebase -i HEAD~3
# Choose "squash" for the last two commits

Suddenly three scattered commits become one polished chapter. Teams often use squash merges in pull requests: the final record shows only “Implement login form” rather than the messy trail of “Add login.html”, “Fix typo”, “Rename field.” Squash tells the story not as it unfolded, but as it should be remembered.

Revert: Correction Without Erasure

Sometimes the past is wrong. A bug slips through, a decision proves faulty. In those moments, git revert offers a way to correct without rewriting. Instead of deleting the bad commit, you create a new one that undoes its changes.

git revert <commit_sha>

The original error remains in history, visible for anyone to see, but its effects are nullified. Philosophically, this is a footnote in the margin: “This was tried, but it was wrong, and here is the correction.” It preserves integrity while acknowledging mistakes.

Reset: Private Revisionism

Then there is git reset, the most radical of all. It moves the branch pointer backwards, discarding commits as if they never happened.

git reset --hard HEAD~1

Used privately, this is a way of tearing out notebook pages before anyone else reads them. It is safe in your local space, a way of keeping drafts clean. But used on a shared branch, it becomes vandalism: history that others relied on is suddenly gone, leaving confusion and broken repositories.

Each of these commands encodes a philosophy. Cherry-pick is selective memory. Squash is curation. Revert is honest correction. Reset is revisionism. Together, they show that Git is not only about storing code; it is about shaping memory. With every action, we decide what should be remembered in detail, what should be compressed into summary, what should be corrected, and what may safely be forgotten.

Team Policies

If Git is a system for writing history, then a development team is a community of historians. And like any community of historians, they must eventually agree on the method by which the past will be remembered. Without such agreement, history fragments: some developers polish their branches with rebases, others scatter the timeline with merges, still others squash everything into single commits. The result is a project whose past is incoherent, a patchwork of competing philosophies.

Healthy teams recognize this and establish conventions. These conventions are not arbitrary; they are reflections of values. Some teams care deeply about traceability. They want every detour, every integration point, every merge preserved. Their logs may be messy, but they are authentic. Other teams care more about readability. They want newcomers to scan history as if it were a carefully edited book. They encourage rebasing and squashing before anything touches main. Neither approach is inherently right or wrong, it is a cultural choice about how history should be written.

In practice, most teams develop a compromise. Local branches are treated like private notebooks: here, rewriting is allowed, even encouraged. A developer may rebase repeatedly, squashing small commits into coherent stories, erasing mistakes before they are shared. But once a branch is made public, once it is pushed to a remote or opened as a pull request, the rules change. Public history is sacrosanct. It should not be rewritten, for others may already be depending on it. At that point, merging or squash-merging becomes the accepted way of reconciliation.

Protected branches such as main or release/* often carry the strictest rules. These are the official archives of the project. They must remain stable, coherent, and trustworthy. Here, resets are forbidden, rebases are off-limits, and only merges, squash merges, or explicit reverts are allowed. Mistakes are corrected with footnotes, not erasures. A bugfix may be reverted, but never silently deleted.

Hotfixes reveal another cultural nuance. In urgent situations, when production is broken and speed matters more than ritual, teams often commit directly to main and then cherry-pick that fix into relevant release branches. This is pragmatic history: not elegant, not ideal, but practical survival. Later, when calm is restored, the story can be rebalanced, squashed, or documented.

Beneath these conventions lies something deeper than mere workflow. They are statements of identity. A team that favors rebasing sees itself as curators of a polished narrative, smoothing the path for future readers. A team that favors merging sees itself as archivists of authentic process, determined to preserve even the rough edges of collaboration. What matters most is coherence, that the team speaks with one voice, so that the history they leave behind can be understood by those who come after.

In this sense, version control is not only about code. It is about culture. The way a team writes its history is a mirror of how it understands itself: as editors of a clean narrative, or as custodians of raw memory. Git gives us the tools, but it is the community that decides how those tools are wielded.

Conclusion

Git teaches us something profound: history is not fixed. It can be recorded in its raw form, polished into a narrative, collapsed into summaries, or even rewritten entirely. With every action, merge, rebase, squash, cherry-pick, revert, reset, we make a decision about how the past will be remembered.

Rebase is the story as you wish it had been told: clean, seamless, curated.
Merge is the story as it happened: branching, converging, messy but faithful. Squash is the act of curation, reducing noise into clarity. Cherry-pick is selective memory, choosing what to carry forward. Revert is correction without concealment. Reset is revisionism, powerful but dangerous.

None of these are inherently right or wrong. Each embodies a philosophy of history. The ethic lies in knowing when each is appropriate. To rebase your own branch before sharing is generosity to your peers; to rebase a public branch is betrayal. To merge recklessly can clutter the archive; to merge thoughtfully honors the record of collaboration. To squash is to curate; to revert is to admit fault; to reset is to tear out pages of a draft.

What Git quietly reminds us is that software engineering is not only about code. It is about collective memory. It is about how we tell the story of our work to those who will come after us, new teammates, maintainers, historians of technology. A project’s Git history is more than a technical log; it is a cultural artifact, a record of human choices under pressure, collaboration across distance, mistakes made and corrected, ambitions pursued and sometimes abandoned.

And so, every time we type git merge or git rebase, we are not only shaping a codebase; we are shaping a narrative. We are choosing whether the past should appear as it truly was, or as we wish it had been.

In the end, Git is a mirror. It reflects how we think about time, memory, and truth. And every developer is, in a sense, a historian, because every commit is a choice about how tomorrow will remember today.

--

--

No responses yet