Lesson 22 · History and integration
Reorder, squash, reword and drop local commits
Turn five rough private commits into two clean ones: move a dependent fix, reword a message, fold and squash steps, drop only the noise, and check that the result has exactly the files you meant.
- Level: Advanced
- 12 min read
- 7 min video
- Updated
- Before this lesson: Rebase unpublished work onto a new base
Progress saved in this browser.
Playing loads the video from YouTube (privacy-enhanced mode).
What you will learn
- Read a replay plan: oldest step first, applied from top to bottom.
- Choose the right action: reword, squash, fixup or drop.
- Move a step without breaking the step it depends on.
- Verify the new commits, the files and the rescue branch afterwards.
Before you start
Your branch private/signup-polish has five commits that exist only on your computer, on top of the commit M. The work tree is clean and rescue/before-polish points at the fifth commit. In order, oldest first:
| Step | Commit | What it does |
|---|---|---|
| C1 | feat: add signup draft | adds the signup form |
| C2 | docs: explain signup review | adds docs/replay-a.md |
| C3 | docs: explain validation | adds docs/replay-b.md |
| C4 | fixup! feat: add signup draft | fixes the form's label |
| C5 | chore: temporary note | adds docs/temporary-review.md |
Everything is local and on the Free plan (GitTree asks you to sign in): no hosting account, no provider, no internet. Every identity is fictional (example.com). Never run this lesson in a real project, and never rewrite commits that other people already have.
Make the changes in GitTree or with the native Git commands, not both on the same copy. Reading the result is always safe. The menu labels come from GitTree 2.0's own strings and the native commands were run with Git 2.43 in disposable copies. The GitTree steps were not run on macOS, Windows or Linux.
Concept and state effects
An interactive rebase is a plan, not a sorting game. Git replays the listed commits one by one on the base, from the top of the list to the bottom, so the oldest step comes first. Each action tells Git what to do with one step:
- Pick keeps the commit as it is.
- Reword keeps its content and lets you write a new message.
- Squash folds the commit into the kept commit above it and lets you write one combined message.
- Fixup folds it in too, but throws its own message away.
- Drop leaves the commit's change out entirely.
Order matters. A step that depends on another (here the label fix needs the form from C1) must stay below it. The editor will let you move it above, and then the replay stops with a conflict. GitTree also refuses plans Git cannot run: a plan that drops every commit, a squash with nothing above it to fold into, and an empty message.
Every replayed commit gets a new ID, because its parent changes. Only this branch is rewritten; the rescue branch keeps the old five. Commits that are already on a remote are marked "On a remote", and rewriting them needs an explicit confirmation. This lesson never confirms that.
The plan you will build turns five commits into two:
| Step | Commit | Action | Result |
|---|---|---|---|
| 1 | C1 | Reword | feat: complete the local signup form |
| 2 | C4 | Fixup | folded into step 1 |
| 3 | C3 | Pick | kept; the squash target |
| 4 | C2 | Squash | docs: explain signup review and validation |
| 5 | C5 | Drop | gone |
Prepare the exact project state
Start from a separate ordinary copy of the project as lesson 18 left it. Leave your original gittree-features untouched. Build the five commits below on a new branch, one file at a time, and save each full body only at its named step.
Complete file inputs
Save only at the phase named below. These are full UTF-8/LF bodies with a final newline; never paste Markdown fences. Unchanged runnable files inherit the named input. Versions in separate variants must not overwrite the original.
index.html — SIGNUP
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitTree Features — course demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="site-header">
<a href="#main">GitTree Features</a>
</header>
<main id="main" tabindex="-1">
<section class="hero" aria-labelledby="hero-title">
<p class="eyebrow">GitTree Learn demo project</p>
<h1 id="hero-title">See your work clearly.</h1>
<p id="hero-copy">Choose one change, then check what Git saved.</p>
</section>
<section class="features" aria-labelledby="features-title">
<h2 id="features-title">Follow your work</h2>
<div class="cards">
<article class="feature-card">
<h3>History</h3>
<p>Read commits and branches together.</p>
</article>
<article class="feature-card">
<h3>Changes</h3>
<p>Review the exact content selected for a commit.</p>
</article>
<article class="feature-card">
<h3>Recovery</h3>
<p>Inspect the scope of a recovery point before using it.</p>
</article>
</div>
</section>
<section aria-labelledby="plans-title">
<h2 id="plans-title">Choose the workflow you need</h2>
<div class="cards">
<article class="feature-card">
<h3>Local Git</h3>
<p>Local Git workflows work without signing in.</p>
</article>
<article class="feature-card">
<h3>Connected workflows</h3>
<p>Provider integrations and Nour require Pro or an applicable trial.</p>
</article>
</div>
</section>
<section aria-labelledby="signup-title">
<h2 id="signup-title">Try the demo form</h2>
<form id="signup-form" novalidate>
<label for="demo-email">Demo email</label>
<input id="demo-email" name="email" type="email" autocomplete="off" aria-describedby="signup-hint">
<p id="signup-hint">Use a fictional example.com address. Nothing is sent.</p>
<button type="submit">Check the address locally</button>
<p id="signup-result" role="status"></p>
</form>
</section>
</main>
<footer><p>Built by a fictional team for GitTree Learn. Review one change at a time.</p></footer>
<script type="module" src="site.js"></script>
</body>
</html>docs/replay-a.md — DOC_A
# Local signup review
Review the label and hint together.docs/replay-b.md — DOC_B
# Local validation review
No address leaves this demonstration.index.html — SIGNUP_LABEL
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitTree Features — course demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="site-header">
<a href="#main">GitTree Features</a>
</header>
<main id="main" tabindex="-1">
<section class="hero" aria-labelledby="hero-title">
<p class="eyebrow">GitTree Learn demo project</p>
<h1 id="hero-title">See your work clearly.</h1>
<p id="hero-copy">Choose one change, then check what Git saved.</p>
</section>
<section class="features" aria-labelledby="features-title">
<h2 id="features-title">Follow your work</h2>
<div class="cards">
<article class="feature-card">
<h3>History</h3>
<p>Read commits and branches together.</p>
</article>
<article class="feature-card">
<h3>Changes</h3>
<p>Review the exact content selected for a commit.</p>
</article>
<article class="feature-card">
<h3>Recovery</h3>
<p>Inspect the scope of a recovery point before using it.</p>
</article>
</div>
</section>
<section aria-labelledby="plans-title">
<h2 id="plans-title">Choose the workflow you need</h2>
<div class="cards">
<article class="feature-card">
<h3>Local Git</h3>
<p>Local Git workflows work without signing in.</p>
</article>
<article class="feature-card">
<h3>Connected workflows</h3>
<p>Provider integrations and Nour require Pro or an applicable trial.</p>
</article>
</div>
</section>
<section aria-labelledby="signup-title">
<h2 id="signup-title">Try the demo form</h2>
<form id="signup-form" novalidate>
<label for="demo-email">Fictional email address</label>
<input id="demo-email" name="email" type="email" autocomplete="off" aria-describedby="signup-hint">
<p id="signup-hint">Use a fictional example.com address. Nothing is sent.</p>
<button type="submit">Check the address locally</button>
<p id="signup-result" role="status"></p>
</form>
</section>
</main>
<footer><p>Built by a fictional team for GitTree Learn. Review one change at a time.</p></footer>
<script type="module" src="site.js"></script>
</body>
</html>docs/temporary-review.md — DOC_NOISE
Temporary rehearsal note, deliberately dropped.In GitTree
Open the palette with Command+Shift+P (Ctrl+Shift+P on Ubuntu). Actions on a commit use that commit's right-click menu.
- Build the five commits and the rescue branch as the next section shows, with
private/signup-polishchecked out and clean. Note M and the five subjects. - Right-click M, the commit just below the five, and choose Edit History → Interactive Rebase of the Commits Above…. The editor opens with the five private steps, oldest first.
- Under Options for this rebase leave all three options off, including Arrange fixup!, squash! and amend! commits. You will move the fixup yourself.
- Focus the C4 row and press Alt+↑ twice, then focus C3 and press Alt+↑ once. The order is now C1, C4, C3, C2, C5. Pressing p, r, e, s, f or d on a focused row sets Pick, Reword, Edit, Squash, Fixup or Drop.
- Set C1 to Reword and type
feat: complete the local signup formin its message field. Set C4 to Fixup. - Keep C3 as Pick and set C2 to Squash. Under Squashed commits the group "2 commits into docs: explain validation" shows a combined message. Replace it with
docs: explain signup review and validation. - Set C5 to Drop. Read every row and the combined message once more. Nothing is marked "On a remote", so no confirmation is needed.
- Choose Start Rebase once and wait. If it stops, use the banner to continue; do not start the whole plan again.
- Verify two new commits, the label fix in the page, both docs and no temporary note. Keep
rescue/before-polish, and leave the work tree clean.
For every ordinary commit in the course: Summary is the stated subject, Description is empty and the options Amend Previous Commit, Skip Hooks, Add Signed-off-by, Different Author, Push After Commit and Allow Empty Commit stay off.
In the terminal
Build the five commits on a new branch, saving each full body at its step:
git switch --create private/signup-polishSave SIGNUP as index.html, then:
git add -- index.html
git commit --quiet -m "feat: add signup draft"Save DOC_A as docs/replay-a.md, then:
git add -- docs/replay-a.md
git commit --quiet -m "docs: explain signup review"Save DOC_B as docs/replay-b.md, then:
git add -- docs/replay-b.md
git commit --quiet -m "docs: explain validation"Save SIGNUP_LABEL as index.html, then:
git add -- index.html
git commit --quiet -m "fixup! feat: add signup draft"Save DOC_NOISE as docs/temporary-review.md, then:
git add -- docs/temporary-review.md
git commit --quiet -m "chore: temporary note"Protect the old tip and list the range, oldest first:
git branch rescue/before-polish
BASE_SHA="$(git rev-parse HEAD~5)"
git log --reverse --format=%s "$BASE_SHA"..HEADNow start the interactive rebase (-i is the short form of --interactive). Git opens your editor with the todo list (your IDs will differ):
git rebase --interactive --no-autosquash --no-update-refs --no-autostash "$BASE_SHA"pick 1a2b3c4 feat: add signup draft
pick 2b3c4d5 docs: explain signup review
pick 3c4d5e6 docs: explain validation
pick 4d5e6f7 fixup! feat: add signup draft
pick 5e6f708 chore: temporary noteRewrite the five lines into the plan, keeping each line's own ID, and save:
reword 1a2b3c4 feat: add signup draft
fixup 4d5e6f7 fixup! feat: add signup draft
pick 3c4d5e6 docs: explain validation
squash 2b3c4d5 docs: explain signup review
drop 5e6f708 chore: temporary noteGit then opens the editor twice more. For the reword, replace the message with feat: complete the local signup form. For the squash, it shows both messages; replace them with one line, docs: explain signup review and validation. Check the result:
git rev-list --count "$BASE_SHA"..HEAD
git log --reverse --format=%s "$BASE_SHA"..HEAD
git status --short2
feat: complete the local signup form
docs: explain signup review and validationThe status is empty. index.html is SIGNUP_LABEL, both docs exist and docs/temporary-review.md does not.
If you put a squash on the first line, Git stops at once with error: cannot 'squash' without a previous commit. Fix the list with git rebase --edit-todo and git rebase --continue, or leave with git rebase --abort. With --autosquash Git does the move and the action for fixup! commits for you; this lesson turns it off to show the plan.
Result
Where you end: two rewritten commits above M, "feat: complete the local signup form" and "docs: explain signup review and validation". The page has the label fix, both docs are there and the temporary note is not. The old five commits are kept by the rescue branch, and the work tree is clean.
Common mistakes
- Using Drop to hide a commit you might need. The change is not in the new history at all.
- Moving a fix above the commit it fixes.
- Squashing the first step, which has nothing above it to fold into.
- Starting the plan again after it finished, instead of checking the result first.
If something goes wrong
Before you start, Cancel leaves the branch as it is. If GitTree says the branch changed since the editor opened, close the editor and open it again. If the rebase stops, fix the conflict or the message and continue from the banner, or abort it deliberately. After it finishes, compare with rescue/before-polish before you try to restore anything.
Check your understanding
Answer each question, then read why. Questions marked “Required” count toward completing the lesson.
0 of 6 correct
Answer the required questions correctly to complete the lesson.
Checked in this browser.
Practice
Explain the plan C1, C4, C3, C2, C5 and the two messages that survive. Check old range 5, new range 2, the expected files and no note. Then predict, without running it, why moving C4 above C1 or squashing step 1 cannot work.
Your own confirmation: the site cannot see your computer.
To complete the lesson, answer the required questions correctly and confirm the practical exercise.
Recap
- A replay plan has an order and dependencies.
- Squash keeps both messages for you to combine; fixup discards its own.
- Drop removes the change itself, so only drop real noise.
Related
Glossary terms introduced
Reword — إعادة صياغة; squash — ضمّ; fixup — ضمّ مع تجاهل الرسالة; drop — إسقاط; replay plan — خطة إعادة تطبيق.
Video transcript
The narration of the video, cue by cue.
Show the transcript
- Five rough commits become two clean ones, safely.
- Interactive rebase is a plan: a list of steps, oldest first.
- Git replays the list from top to bottom on a base.
- Each step gets an action: pick, reword, squash, fixup or drop.
- First, build five private commits and a rescue branch.
- Three are real work. One is a fix, and one is temporary noise.
- Right-click the commit below them, and open Edit History.
- Choose Interactive Rebase of the Commits Above.
- The editor lists the five steps, oldest first.
- Leave all three options off. We will move the fixup ourselves.
- The fixup belongs right after the commit it fixes. Move it up twice.
- Now move the validation note above the review note.
- The order is now one, four, three, two, five.
- Set the first step to Reword, and write the new message.
- Set the fixup step to Fixup. Its message is thrown away.
- Keep the validation note, and squash the review note into it.
- Squash shows both messages. Write one message for the pair.
- Drop the temporary note. Its change will not survive.
- Read every row once more. Nothing is on a remote.
- Choose Start Rebase once, and wait for it to finish.
- The graph now has two commits above the base.
- Both have new IDs. The old five are on the rescue branch.
- Open the page: the label fix is there. The noise is gone.
- Now the same plan in the terminal, with git rebase -i.
- Git opens the todo list. Oldest first, all set to pick.
- Rewrite the lines into our plan: reword, fixup, pick, squash, drop.
- Git asks for the two messages. Write one line for each.
- Count the commits above the base: two.
- List the subjects. These are the two we wrote.
- Status is empty, and the temporary note is gone.
- Change the input: put the squash on the first step.
- There is nothing above it to fold into, so Git refuses.
- Or move the fixup above its commit: the replay stops with a conflict.
- If it stops, fix it and continue. Or abort, and nothing changes.
- The rescue branch still holds your original five commits.
- Never rewrite commits that someone else already has.
- Practice: explain the plan, and why step one cannot be a squash.
- Next, resolve conflicts when a merge or a rebase stops.