<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Amit</title>
    <description>The latest articles on DEV Community by Amit (@amitg01).</description>
    <link>https://dev.to/amitg01</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F244254%2Fe439200f-0001-437d-ad8b-01b3c9f8faf7.png</url>
      <title>DEV Community: Amit</title>
      <link>https://dev.to/amitg01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amitg01"/>
    <language>en</language>
    <item>
      <title>The Power of git rebase --onto: Surgical Commit Slicing in Git</title>
      <dc:creator>Amit</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:50:11 +0000</pubDate>
      <link>https://dev.to/amitg01/the-power-of-git-rebase-onto-surgical-commit-slicing-in-git-4h7m</link>
      <guid>https://dev.to/amitg01/the-power-of-git-rebase-onto-surgical-commit-slicing-in-git-4h7m</guid>
      <description>&lt;p&gt;If you work in an engineering team that uses stacked branches or a &lt;strong&gt;Squash and Merge&lt;/strong&gt; PR workflow, you have almost certainly encountered this stomach-dropping nightmare:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You create branch &lt;code&gt;feature-B&lt;/code&gt; off &lt;code&gt;feature-A&lt;/code&gt; while waiting for code review.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feature-A&lt;/code&gt; gets approved and merged into &lt;code&gt;main&lt;/code&gt; using GitHub's or GitLab's &lt;strong&gt;Squash &amp;amp; Merge&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You change the target of your &lt;code&gt;feature-B&lt;/code&gt; pull request to &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaster strikes:&lt;/strong&gt; Your PR suddenly shows 25 commits instead of 3, hundreds of lines of duplicate diff you didn't write, and conflicting files you never touched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Naturally, your first instinct is to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout feature-B
git rebase main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...only to be greeted by a terrifying stream of merge conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Does Standard Rebase Fail Here?
&lt;/h3&gt;

&lt;p&gt;When GitHub or GitLab squashes &lt;code&gt;feature-A&lt;/code&gt;, it bundles all its changes into a &lt;strong&gt;single brand-new commit&lt;/strong&gt; on &lt;code&gt;main&lt;/code&gt; with a brand-new SHA hash. &lt;/p&gt;

&lt;p&gt;A standard &lt;code&gt;git rebase main&lt;/code&gt; looks backward from &lt;code&gt;feature-B&lt;/code&gt; to find the common ancestor with &lt;code&gt;main&lt;/code&gt;. Because the individual commit hashes from &lt;code&gt;feature-A&lt;/code&gt; don't exist on &lt;code&gt;main&lt;/code&gt;, Git assumes those commits are new changes you authored, and tries to replay them one-by-one—colliding directly with the squash commit that already contains those exact changes.&lt;/p&gt;

&lt;p&gt;This is where Git's most powerful, under-utilized precision tool comes in: &lt;strong&gt;&lt;code&gt;git rebase --onto&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mental Model: Scissors &amp;amp; Glue
&lt;/h2&gt;

&lt;p&gt;A standard rebase says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Take everything on my branch since it diverged from base, and replay it on top of new base."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;--onto&lt;/code&gt; flag gives you &lt;strong&gt;precision scissors&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Take only the commits between point X and point Y, cut them out, and glue them onto point Z."&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;--onto&lt;/span&gt; &amp;lt;new-base&amp;gt; &amp;lt;old-base&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;branch&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;What it represents&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;th&gt;Boundary Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;new-base&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where commits will land&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;table&lt;/strong&gt; where you paste the slice&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Target base&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;old-base&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The cutoff point where slice begins&lt;/td&gt;
&lt;td&gt;Where you put the &lt;strong&gt;first scissor cut&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Exclusive&lt;/strong&gt; (not copied)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[&amp;lt;branch&amp;gt;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tip of the commit slice&lt;/td&gt;
&lt;td&gt;Where you put the &lt;strong&gt;second scissor cut&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Inclusive&lt;/strong&gt; (defaults to &lt;code&gt;HEAD&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In set notation: Git replays &lt;code&gt;(old-base .. branch]&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Visualizing the Problem &amp;amp; The Fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Squash-Merge Trap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Squashed &amp;amp; merged via GitHub UI
                ┌─────────────────────────────────┐
                │                                 ▼
main:        o───o─────────────────────────────────S (Squash Commit: SHA '9f2a')
              \
feature-A:     A1───A2───A3 (SHA '1111' .. '3333')
                         \
feature-B:                B1───B2 (HEAD)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because commit &lt;code&gt;S&lt;/code&gt; has hash &lt;code&gt;9f2a&lt;/code&gt;, Git doesn't know that &lt;code&gt;A1&lt;/code&gt;, &lt;code&gt;A2&lt;/code&gt;, and &lt;code&gt;A3&lt;/code&gt; are already inside &lt;code&gt;S&lt;/code&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Surgical Suture
&lt;/h3&gt;

&lt;p&gt;You want Git to discard &lt;code&gt;A1..A3&lt;/code&gt; and only grab &lt;code&gt;B1&lt;/code&gt; and &lt;code&gt;B2&lt;/code&gt;, pasting them onto &lt;code&gt;S&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout feature-B
git fetch origin

&lt;span class="c"&gt;# Syntax: git rebase --onto &amp;lt;where-to-land&amp;gt; &amp;lt;where-to-cut-from&amp;gt; &amp;lt;tip&amp;gt;&lt;/span&gt;
git rebase &lt;span class="nt"&gt;--onto&lt;/span&gt; origin/main origin/feature-A feature-B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Clean Result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main:        o───o───S───B1'───B2' (feature-B)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No duplicate commits. Zero false diffs in your PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  3 Real-World Scenarios with Concrete Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario 1: The Stacked PR After Squash Merge (The Classic)
&lt;/h3&gt;

&lt;p&gt;Let's look at what your terminal actually looks like in this scenario.&lt;/p&gt;

&lt;p&gt;Suppose your &lt;code&gt;git log --oneline&lt;/code&gt; on &lt;code&gt;feature-B&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f6a1b2c (HEAD -&amp;gt; feature-B) Add checkout UI tests
e5d4c3b Add payment checkout UI
3a2b1c0 (origin/feature-A) Finalize Stripe client
2f1e0d9 Add Stripe webhooks
1c2b3a4 Setup Stripe API keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commits &lt;code&gt;1c2b3a4&lt;/code&gt;, &lt;code&gt;2f1e0d9&lt;/code&gt;, and &lt;code&gt;3a2b1c0&lt;/code&gt; are from &lt;code&gt;feature-A&lt;/code&gt;, which was just squash-merged into &lt;code&gt;main&lt;/code&gt; as commit &lt;code&gt;888aaaa&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Command:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin
git rebase &lt;span class="nt"&gt;--onto&lt;/span&gt; origin/main origin/feature-A feature-B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify the result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a1b2c3d (HEAD -&amp;gt; feature-B) Add checkout UI tests
9e8d7c6 Add payment checkout UI
888aaaa (origin/main) Merge pull request #101 from feature-A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, surgical, and conflict-free.&lt;/p&gt;




&lt;h3&gt;
  
  
  Scenario 2: Slicing Out Broken or Experimental Commits
&lt;/h3&gt;

&lt;p&gt;Imagine you were hacking away on a feature, added experimental debugging/mocking code, and later wrote clean implementation commits on top. You want to ditch commits &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;D&lt;/code&gt; without entering an interactive rebase editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A ─── B ─── C (bad mock) ─── D (dirty debug) ─── E (clean impl) ─── F (clean tests - HEAD)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to keep &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, &lt;code&gt;E&lt;/code&gt;, and &lt;code&gt;F&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Command:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;--onto&lt;/span&gt; B D HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Remember the exclusive rule:&lt;/em&gt; &lt;code&gt;&amp;lt;old-base&amp;gt;&lt;/code&gt; is &lt;code&gt;D&lt;/code&gt;. Git takes everything &lt;em&gt;after&lt;/em&gt; &lt;code&gt;D&lt;/code&gt; up to &lt;code&gt;HEAD&lt;/code&gt; (&lt;code&gt;E&lt;/code&gt; and &lt;code&gt;F&lt;/code&gt;) and grafts them directly on top of &lt;code&gt;B&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A ─── B ─── E' ─── F' (HEAD)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Scenario 3: Splitting a Monolithic 10-Commit Branch into Two Clean PRs
&lt;/h3&gt;

&lt;p&gt;You got carried away and implemented database migrations, backend services, and frontend UI all in one branch: &lt;code&gt;mega-feature&lt;/code&gt; (10 commits).&lt;/p&gt;

&lt;p&gt;Your team lead asks: &lt;em&gt;"Can you split this into two PRs? One for Backend, one for Frontend?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Inspect your log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* 9999999 (HEAD -&amp;gt; mega-feature) [FE] Connect form to API
* 8888888 [FE] Build dashboard UI
* 7777777 [FE] Add UI state management
* 6666666 [BE] Expose REST endpoints
* 5555555 [BE] Add service layer logic       &amp;lt;-- Split point!
* 4444444 [BE] Add database migrations
* ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 1: Branch off Part 1 (Backend)
&lt;/h4&gt;

&lt;p&gt;Point a branch at commit &lt;code&gt;5555555&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch feature-part1-backend 5555555
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Extract Part 2 (Frontend) onto &lt;code&gt;main&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Now slice out commits &lt;code&gt;6666666&lt;/code&gt; through &lt;code&gt;HEAD&lt;/code&gt; and rebase them onto &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout mega-feature
git rebase &lt;span class="nt"&gt;--onto&lt;/span&gt; main feature-part1-backend mega-feature
git branch &lt;span class="nt"&gt;-m&lt;/span&gt; mega-feature feature-part2-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;feature-part1-backend&lt;/code&gt; ready for review with commits 1–5.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feature-part2-frontend&lt;/code&gt; ready to stack on top of part 1 or target &lt;code&gt;main&lt;/code&gt; independently.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pro-Tips &amp;amp; Safety Nets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Exclusive Boundary Trap (The Parent &lt;code&gt;^&lt;/code&gt; Trick)
&lt;/h3&gt;

&lt;p&gt;If you want to move a specific single commit &lt;code&gt;X&lt;/code&gt; onto &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running &lt;code&gt;git rebase --onto main X branch&lt;/code&gt; will &lt;strong&gt;exclude&lt;/strong&gt; commit &lt;code&gt;X&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Instead, run: &lt;code&gt;git rebase --onto main X^ branch&lt;/code&gt; (using &lt;code&gt;X^&lt;/code&gt; targets the parent of &lt;code&gt;X&lt;/code&gt;, ensuring &lt;code&gt;X&lt;/code&gt; is included).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Always Verify Before Pushing
&lt;/h3&gt;

&lt;p&gt;Before force-pushing, inspect the exact commits that will go up to the remote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log origin/main..HEAD &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see anything you didn't write or expect, stop and inspect.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Safety Net: The Undo Button (&lt;code&gt;git reflog&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Mess up your cut? Git does not delete commits immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reflog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the entry right before you ran the rebase (e.g., &lt;code&gt;HEAD@{1}: checkout: moving from...&lt;/code&gt;), then reset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD@&lt;span class="o"&gt;{&lt;/span&gt;1&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are immediately back to where you started.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Always Push with Lease
&lt;/h3&gt;

&lt;p&gt;Rewriting commit history requires a force push. Avoid bare &lt;code&gt;git push -f&lt;/code&gt;. Always use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--force-with-lease&lt;/code&gt; guarantees that if a teammate pushed changes to your branch while you were rebasing, Git aborts rather than overwriting their work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary Cheat Sheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# When feature-A was squashed into main:&lt;/span&gt;
git rebase &lt;span class="nt"&gt;--onto&lt;/span&gt; origin/main origin/feature-A feature-B

&lt;span class="c"&gt;# Drop commits between commit-X and commit-Y:&lt;/span&gt;
git rebase &lt;span class="nt"&gt;--onto&lt;/span&gt; &amp;lt;commit-X&amp;gt; &amp;lt;commit-Y&amp;gt; HEAD

&lt;span class="c"&gt;# Verify your slice:&lt;/span&gt;
git log origin/main..HEAD &lt;span class="nt"&gt;--oneline&lt;/span&gt;

&lt;span class="c"&gt;# Push safely:&lt;/span&gt;
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin feature-B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Have you had to rescue a stacked PR recently? Drop your favorite Git surgical tricks in the comments below!&lt;/p&gt;

</description>
      <category>git</category>
      <category>devops</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
