Skip to content

History Rewriting - Interactive Rebase & Maintenance 🔄

ROLE: HPN (Git Mastery).
AUDIENCE: Engineers muốn có clean history và repository hiệu quả.

Git history không phải chỉ để đọc - nó có thể được viết lại. Module này dạy bạn cách reshape history để có commits sạch, meaningful, và repository được tối ưu.


🎯 Mục tiêu

Sau module này, bạn sẽ:

  • Master Interactive Rebase - squash, reword, drop, split commits
  • Hiểu The Golden Rule - khi nào KHÔNG được rewrite history
  • Setup Rerere - Git tự nhớ cách bạn resolve conflicts
  • Dùng Garbage Collection - dọn dẹp và nén repository

⚠️ The Golden Rule

NEVER REWRITE PUBLIC HISTORY

Không bao giờ rewrite history đã được push lên shared branch (main, develop).

Rewriting public history = Đồng nghiệp pull về và gặp:

  • Conflict vô lý
  • Lost commits
  • "Why is my branch 50 commits behind AND ahead?"

Chỉ rewrite history trên LOCAL branches hoặc feature branches chưa ai pull.


🔧 Interactive Rebase (git rebase -i)

The Swiss Army Knife of Git

Interactive rebase cho phép bạn chỉnh sửa, gộp, xóa, sắp xếp lại các commits.

bash
# Rebase 5 commits gần nhất
git rebase -i HEAD~5

# Rebase từ một commit cụ thể
git rebase -i abc123

# Rebase từ root (toàn bộ history)
git rebase -i --root

The Interactive Editor

Khi chạy git rebase -i, Git mở editor với danh sách commits:

pick abc1234 feat: add login form
pick def5678 fix: typo in button
pick ghi9012 wip: trying something
pick jkl3456 fix: actually working now
pick mno7890 feat: add validation

# Rebase abc1234..mno7890 onto xyz0000 (5 commands)
#
# Commands:
# p, pick   = use commit
# r, reword = use commit, but edit the commit message
# e, edit   = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup  = like "squash", but discard this commit's log message
# d, drop   = remove commit
# x, exec   = run command

🔀 Technique 1: Squash (Gộp Commits)

Problem: 5 commits nhỏ lẻ cho 1 feature → messy history

Solution: Squash thành 1 commit meaningful

bash
git rebase -i HEAD~5

Before:

pick abc1234 feat: add login form
pick def5678 fix: typo in button
pick ghi9012 wip: trying something
pick jkl3456 fix: actually working now
pick mno7890 feat: add validation

Change to:

pick abc1234 feat: add login form
squash def5678 fix: typo in button
squash ghi9012 wip: trying something
squash jkl3456 fix: actually working now
squash mno7890 feat: add validation

Result: Editor mở để bạn viết combined commit message.

FIXUP vs SQUASH

  • squash: Gộp commit VÀ mở editor để edit message
  • fixup: Gộp commit VÀ discard message của commit đó

Dùng fixup khi commit là "WIP" hoặc "typo fix" không cần giữ message.


✏️ Technique 2: Reword (Sửa Message Cũ)

Problem: Commit message typo hoặc không đúng format

bash
git rebase -i HEAD~3

Change:

reword abc1234 feat: add lgin form
pick def5678 fix: validation
pick ghi9012 feat: add logout

Result: Git dừng lại để bạn edit message của commit đầu tiên.


🗑️ Technique 3: Drop (Xóa Commit)

Problem: Commit debug code nhầm, muốn xóa hoàn toàn

bash
git rebase -i HEAD~4

Change:

pick abc1234 feat: add feature
drop def5678 debug: console.log everywhere
pick ghi9012 fix: cleanup
pick jkl3456 feat: more features

Result: Commit def5678 bị xóa khỏi history như chưa từng tồn tại.


✂️ Technique 4: Split (Tách Commit)

Problem: Một commit làm quá nhiều thứ, muốn tách ra

bash
git rebase -i HEAD~3

Change:

edit abc1234 feat: add login and signup and profile
pick def5678 fix: something
pick ghi9012 feat: another

Khi Git dừng lại ở commit cần split:

bash
# 1. Undo commit nhưng giữ changes
git reset HEAD^

# 2. Stage và commit từng phần
git add src/login.js
git commit -m "feat: add login"

git add src/signup.js
git commit -m "feat: add signup"

git add src/profile.js
git commit -m "feat: add profile"

# 3. Continue rebase
git rebase --continue

🔄 Technique 5: Reorder (Sắp Xếp Lại)

Problem: Commits không theo thứ tự logic

bash
git rebase -i HEAD~4

Before:

pick abc1234 feat: add UI
pick def5678 feat: add API
pick ghi9012 fix: UI bug
pick jkl3456 fix: API bug

Reorder to:

pick def5678 feat: add API
pick jkl3456 fix: API bug
pick abc1234 feat: add UI
pick ghi9012 fix: UI bug

Handling Conflicts During Rebase

bash
# Khi có conflict:
# 1. Resolve conflict trong files
# 2. Stage resolved files
git add <resolved-files>

# 3. Continue
git rebase --continue

# Hoặc abort nếu muốn hủy
git rebase --abort

# Skip commit hiện tại (nguy hiểm)
git rebase --skip

🧠 Git Rerere - Remember Conflict Resolutions

The Problem

"Rebase feature branch lên main. Resolve 5 conflicts. Abort vì lý do nào đó. Rebase lại. Phải resolve 5 conflicts lại từ đầu."

The Solution: Rerere

Rerere = REuse REcorded REsolution

Git ghi nhớ cách bạn resolve conflicts, tự động apply lần sau.

bash
# Enable rerere (một lần)
git config --global rerere.enabled true

# Xem recorded resolutions
ls .git/rr-cache/

# Forget a resolution
git rerere forget <file>

# Clear all resolutions
rm -rf .git/rr-cache

How It Works

Rerere in Action

bash
# 1. Enable rerere
git config rerere.enabled true

# 2. First time conflict
git merge feature
# CONFLICT in auth.py
# ... resolve manually ...
git add auth.py
git commit -m "merge: feature branch"
# rerere: Recorded resolution for 'auth.py'

# 3. Later, abort and retry
git reset --hard HEAD^
git merge feature
# CONFLICT in auth.py
# rerere: Resolved 'auth.py' using previous resolution. ✅
git add auth.py
git commit -m "merge: feature branch"

RERERE USE CASES

  1. Repeated rebases trên long-running feature branch
  2. Test merges - thử merge, resolve, abort, merge lại sau
  3. CI/CD - pre-test merge conflicts

🗑️ Git Garbage Collection

What Gets Collected?

Unreachable objects:

  • Commits sau git reset --hard
  • Commits từ deleted branches
  • Old versions của amended files
  • Leftover từ failed rebases

When Does GC Run?

bash
# Git tự động chạy gc sau ~7000 loose objects
# Hoặc manual:

# Basic garbage collection
git gc

# Aggressive (slower, more thorough)
git gc --aggressive

# Prune unreachable objects NGAY (thay vì đợi 2 tuần)
git gc --prune=now

# Xem gc stats
git count-objects -vH

Understanding Packfiles

bash
# Loose objects
.git/objects/
├── a1/b2c3d4...  # Each object is a separate file
├── e5/f6g7h8...
└── i9/j0k1l2...

# After gc → Packfiles
.git/objects/pack/
├── pack-abc123.pack  # All objects compressed into one file
└── pack-abc123.idx   # Index for fast lookup

Benefits:

  • Delta compression (chỉ lưu diff giữa similar objects)
  • Faster clones (ít files hơn)
  • Smaller repo size

Manual Cleanup Commands

bash
# Remove old reflog entries
git reflog expire --expire=30.days --all

# Prune unreachable objects
git prune

# Repack objects
git repack -a -d --depth=250 --window=250

# The nuclear option (careful!)
git gc --aggressive --prune=now

Check Repository Health

bash
# Verify integrity
git fsck

# Verify with more detail
git fsck --full

# Count objects
git count-objects -vH
# Output:
# count: 42           ← loose objects
# size: 168.00 KiB    ← loose objects size
# in-pack: 12345      ← packed objects
# size-pack: 45.20 MiB
# prune-packable: 0
# garbage: 0
# size-garbage: 0 bytes

📊 Quick Reference

Interactive Rebase Actions

ActionKeywordEffect
Keep commitpick (p)Use commit as-is
Edit messagereword (r)Stop to edit message
Combinesquash (s)Meld into previous, edit combined message
Combine quietlyfixup (f)Meld into previous, discard message
Removedrop (d)Delete commit from history
Pauseedit (e)Stop for amending/splitting
Run commandexec (x)Run shell command

Emergency Escape

bash
# Abort any rebase in progress
git rebase --abort

# Recover from bad rebase (use reflog)
git reflog
git reset --hard HEAD@{5}  # Go back to safe state

💡 Key Takeaways

HPN'S INSIGHT

"Clean history không phải vanity - nó là documentation. 'WIP commit 47' không giúp ai debug 6 tháng sau."

  1. Golden Rule là absolute: KHÔNG rewrite public history
  2. Squash liberally: Feature = 1 commit (hoặc vài commits có ý nghĩa)
  3. Reword freely: Typo trong message? Fix it trước khi push
  4. Rerere saves time: Enable globally, forget about repeated conflicts
  5. GC là automatic: Nhưng biết cách manual run khi cần

Workflow Recommendation

bash
# Trước khi push feature branch:
git rebase -i main   # Clean up history
git push origin feature/my-feature

# Hoặc nếu đã push (branch chỉ mình bạn dùng):
git rebase -i main
git push --force-with-lease origin feature/my-feature

FORCE PUSH SAFELY

Luôn dùng --force-with-lease thay vì --force:

  • --force: Overwrite bất kể
  • --force-with-lease: Fail nếu remote có commits bạn chưa có

Safer, nhưng vẫn nguy hiểm trên shared branches!

🧠 Quiz

Câu 1: Tại sao nên dùng --force-with-lease thay vì --force khi push sau rebase?

  • [ ] A) Vì --force-with-lease nhanh hơn
  • [x] B) Vì --force-with-lease fail nếu remote có commits mới mà bạn chưa pull, tránh overwrite code người khác
  • [ ] C) Vì --force đã bị deprecated
  • [ ] D) Vì --force-with-lease tự động backup trước khi push

💡 Giải thích: --force ghi đè bất kể tình trạng remote. --force-with-lease kiểm tra xem remote có commits mới không - nếu có, nó từ chối push. Đây là "safety net" tránh vô tình xóa code của đồng nghiệp.

Câu 2: git commit --amend thay đổi điều gì?

  • [ ] A) Tạo một commit mới hoàn toàn độc lập
  • [x] B) Thay thế commit cuối cùng bằng commit mới (SHA thay đổi)
  • [ ] C) Chỉ sửa commit message mà không thay đổi SHA
  • [ ] D) Merge commit cuối với commit trước đó

💡 Giải thích: --amend tạo commit MỚI thay thế commit cuối (SHA thay đổi vì nội dung khác). Có thể sửa message, thêm/bớt file. Chỉ dùng cho commit chưa push, vì nó viết lại lịch sử.

Câu 3: Trong Interactive Rebase (git rebase -i), action "squash" làm gì?

  • [ ] A) Xóa commit khỏi lịch sử
  • [ ] B) Đổi thứ tự commit
  • [x] C) Gộp commit vào commit trước đó, kết hợp cả hai messages
  • [ ] D) Đổi tên commit message

💡 Giải thích: squash gộp commit hiện tại vào commit phía trên, cho phép bạn edit combined message. fixup tương tự nhưng bỏ message của commit bị gộp. Rất hữu ích để dọn dẹp "WIP" commits trước khi merge.