Mental Model

A Gerrit review is a commit, not a branch.

main
 └─ A  ← Review A
     └─ B  ← Review B
         └─ C  ← Review C
             └─ D  ← Review D

Each commit has its own Change-Id.


Inspect History

git log --oneline --graph --decorate --all

Example:

* dddddd (HEAD -> feature) Review D
* cccccc Review C
* bbbbbb Review B
* aaaaaa Review A
* 111111 (origin/main, main)

Update the Top Review

Current:

A
 └─ B
     └─ C
         └─ D

Need to fix Review D:

# edit files
git add .
git commit --amend
git push origin HEAD:refs/for/main

No rebase required.


Update a Non-Top Review

Current:

A
 └─ B
     └─ C
         └─ D

Need to fix Review B:

git rebase -i main

Change:

pick A
pick B
pick C
pick D

to:

pick A
edit B
pick C
pick D

Save and quit.

Git stops at B.

Fix and amend:

# edit files
 
git add .
git commit --amend

Continue:

git rebase --continue

Push:

git push origin HEAD:refs/for/main

Result:

A
 └─ B'
      └─ C'
           └─ D'

Rebase Commands

Modify Commit

pick → edit

Example:

pick A
edit B
pick C

Reorder Commits

Move lines.

Before:

pick A
pick B
pick C

After:

pick A
pick C
pick B

Use carefully.


Squash Commits

Before:

pick A
pick B

After:

pick A
squash B

Result:

A+B

single commit.


Remove Commit

Before:

pick A
pick B
pick C

After:

pick A
drop B
pick C

or delete the line.


How Much Gets Rewritten?

Stack:

A
 └─ B
     └─ C
         └─ D
Edited CommitRewritten
DD
CC, D
BB, C, D
AA, B, C, D

Rule:

Editing a commit rewrites that commit and everything above it.


Gerrit Change-Id Rule

Always keep:

Change-Id: Ixxxxxxxx

inside commit messages.

Good:

git commit --amend

and keep existing Change-Id.

Bad:

Delete Change-Id

Gerrit creates a new review.


Common Gerrit Workflow

Create Review A

git checkout -b feature
 
# changes
git commit
 
git push origin HEAD:refs/for/main

Create Review B on top of A

# more changes
git commit
 
git push origin HEAD:refs/for/main

Result:

Review B
   depends on
Review A

Fix Review A Later

git rebase -i main
edit A
pick B
git commit --amend
git rebase --continue
git push origin HEAD:refs/for/main

Useful Aliases

git config --global alias.lg \
"log --oneline --graph --decorate --all"

Usage:

git lg

Quick Memory Trick

Think of the stack as a tower:

D
C
B
A

If you modify a block:

  • Everything above it must be rebuilt.

  • Everything below it remains unchanged.

Moving Changes Between Commits

Scenario

Current stack:

main
 └─ A      (Review A)
     └─ B  (Review B)

Reviewer says:

Change X currently in B should belong to A.

Example:

A = Add API
 
B = Use API
  + change X in file_x.cpp

Reviewer wants:

A = Add API
  + change X
 
B = Use API

Common Confusion

You are on branch:

featureX

main
 └─ A
     └─ B

When editing A:

git rebase -i main
edit A
pick B

you still see change X in your files.

This is normal.

Reason:

The working tree always contains:

A + B

because your branch points to the top of the stack.


Important Mental Model

A commit is a diff, not a snapshot.

Current files:

Working Tree = A + B

What matters is:

git show A
git show B

which tells Git:

What changes belong to A?
What changes belong to B?

Inspect Commit Contents

Show commit A:

git show A

Show commit B:

git show B

Show only file_x changes in B:

git show B -- file_x.cpp

Compare commit against its parent:

git diff B^..B

Correct Procedure

Step 1 - Start Interactive Rebase

git rebase -i main

Before:

pick A
pick B

After:

edit A
edit B

Step 2 - Move Change X into A

Git stops at A.

Apply change X to A.

Example:

git add file_x.cpp
git commit --amend

Now:

A' = A + change X

Continue:

git rebase --continue

Step 3 - Remove Change X from B

Git stops at B.

Remove the lines that were moved into A.

Amend:

git add .
git commit --amend

Continue:

git rebase --continue

Result:

main
 └─ A'
      └─ B'

Where:

A' = old A + change X
 
B' = old B - change X

Alternative: Conflict During Replay

Sometimes:

edit A
pick B

is enough.

After amending A:

A'

Git replays B.

Because B still contains change X:

Conflict

may occur.

Resolve by:

Keep change X in A
Remove change X from B

Then:

git add .
git rebase --continue

Why Cherry-Picking A Worked

You created:

new_branch
 └─ A

without B.

Therefore:

Working Tree = A only

which made the change easier to reason about.

This is valid but not required.

Interactive rebase can perform the same surgery inside the stack.


Quick Rule

When reviewer says:

Move code from commit B into commit A

Think:

1. Edit A
2. Add code to A
3. Edit B
4. Remove code from B
5. Continue rebase
6. Push updated stack

Result:

Before:
 
A
 └─ B(change X)
 
After:
 
A(change X)
 └─ B(without change X)

Useful Commands

git log --oneline --graph --decorate
git show <commit>
git diff <commit>^..<commit>
git rebase -i main
git commit --amend
git rebase --continue
git push origin HEAD:refs/for/main

Key Lesson

Seeing change X in the files while editing A does not mean change X belongs to A.

Always remember:

Working Tree = cumulative result of the stack
 
Commit = diff relative to parent

Reviewers care about the commit diff, not the final file contents.