Skip to content

Instantly share code, notes, and snippets.

@OleksiyRudenko
Last active October 26, 2020 18:15
Show Gist options
  • Save OleksiyRudenko/232e1ebe6ed0780fc69d7391723cc75b to your computer and use it in GitHub Desktop.
Save OleksiyRudenko/232e1ebe6ed0780fc69d7391723cc75b to your computer and use it in GitHub Desktop.
Make use of git rebase

Extracted from git-rebase-illustration and refined

Git Rebase Techniques

At this stage it is not so important to follow the history of commits, therefore early commit messages are used to easily identify commits (e.g. A, B etc.) and their rebased variations (e.g. A', B', B'' etc.)

Decorated one-line logs of this repo are used to illustrate the effect of rebase operations.

Legend

  • A, B, B1, B2 etc. - commits
  • A', A'', A1' etc. - rebased commits (A' is a version of A, A'' is a version of A')
  • dev - branch name

Initial state

There is some story of project development and master represents released product, with latest commits A and B. At B new development branch dev has started.

Other changes (e.g. documentation) were introduced to master (D).

We are on dev.

*        e9378e5 C (HEAD -> dev)
| *      e58c3c0 D (origin/master, master)
|/  
*        bdef04a B
*        9fa8598 A
*        6b2487d Init

Case 1. Sync with remote

We've done some changes F that we want to push, but it appears that meanwhile someone has already updated remote with E.

git push will require to merge in remote changes before any push.

git pull will most likely cause a merge conflict (which you may want to abort with git merge --abort).

The proper way would be git pull --rebase or git fetch && git rebase origin/dev. git push to update the remote immediately.

Anyway if you have forgotten to rebase on pull, do the following

  1. git merge --abort (tree on the left)
  2. git rebase origin/dev (tree on the right)
*   65dfd56 F (HEAD -> dev)              *   b7200ef F'(HEAD -> dev)
| * 0b160f5 E (origin/dev)               *   0b160f5 E (origin/dev)
|/                                       |
| * e58c3c0 D (origin/master, master)    | * e58c3c0 D (origin/master, master)
* | 801f263 C                            * | 801f263 C
|/                                       |/
*   bdef04a B                            *   bdef04a B
*   9fa8598 A                            *   9fa8598 A
*   6b2487d Init                         *   6b2487d Init

(actual git log output above and further down is slightly amended for the sake of readability)

Note how your commits F and F' are different on SHA.

Should you anyway run into merger conflict:

  1. Resolve conflicts
  2. git add <changed-files>
  3. git rebase --continue to complete

Case 2. Tiding up

You work on a feature on its own branch fea01. Every addition and amendment has its own commit. Finally you feel code is good enough (tested, etc) to be merged.

You find out that commits G1..G4 are associated with class (e.g.) Golf, H1..H3 associated with class Hockey, and I1..I4 refer to some core features (Internals).

*    c051b13 I4 (HEAD -> fea01)
*    0324a66 H3
*    b23329b I3
*    b91896e G4
*    0b2fd60 I2
*    f9f844d H2
*    fbff5ee I1
*    5f382e6 G3
*    01664cf G2
*    13ee55b H1
*    ede1d37 G1
*    acc1c91 F' (origin/dev, dev)
*    0b160f5 E
| *  e58c3c0 D (origin/master, master)
* |  801f263 C
|/
*    bdef04a B
*    9fa8598 A
*    6b2487d Init

However commits related to an entity are scattered across the branch. It is sensible to group commits per entity and to probably even squash commits related to the same entity for it doesn't make sense to keep track for every tiny change anymore (especially if some part of code has evolved and its early versions are obsolete and useless).

Targets

Bring some order, squash commits. Have a back up to start over.

Rebase script

Rebasing commits on a branch has a side-effect:

 ...-G-H-I-J-K
             ^ feature-branch
			 
 git rebase -i HEAD~4              # (J-K-I-H)
 
 ...-G-J'-K'-I'-H'
                ^ feature-branch

Where are H, I, J, K? They are gone (almost), there is no reference (branch or tag) to track them.

Just create a branch pointer at current position to be able to undo things easily.

git checkout fea01
# Create a back-up pointer
git branch fea01-backup
# Rebase last 11 commits; 
# alternatively refer to a particular SHA, e.g. acc1c91 (refers to F')
git rebase -i HEAD~11

# here is what git offers as a command list
pick becaf4d G1
pick b74e607 H1
pick 44db105 G2
pick 6a5edee G3
pick 2ec4aa5 I1
pick 91c528f H2
pick 427403a I2
pick 25c5f5a G4
pick 09def4c I3
pick 6406b2e H3
pick 9d4219f I4
# you see commits listed in chronological order (from earliest on the top to the latest at the bottom)

# bring some order grouping commits per each feature
pick becaf4d G1
pick 44db105 G2
pick 6a5edee G3
pick 25c5f5a G4
pick b74e607 H1
pick 91c528f H2
pick 6406b2e H3
pick 2ec4aa5 I1
pick 427403a I2
pick 09def4c I3
pick 9d4219f I4

# squash (retaining only message for the first commit in a group)
pick becaf4d G1
fixup 44db105 G2
fixup 6a5edee G3
fixup 25c5f5a G4
pick b74e607 H1
fixup 91c528f H2
fixup 6406b2e H3
pick 2ec4aa5 I1
fixup 427403a I2
fixup 09def4c I3
fixup 9d4219f I4
# Save and quit the editor

# Resolve conflicts if any
# You may run into commit --allow-empty warning if you were undoing
# changes by means of committing deletions of what was added before
git commit --allow-empty -m "abc..."

# Or you may be required to resolve conflicts manually.
# When done stage files
git add README.md

# Whenever git rebase prompts for an additional action
# when done do:
git rebase --continue

# Redo the above until success reported

# At any stage until success reported you may revert to the initial state
git rebase --abort

Notes:

  • pick : keep commit as is
  • reword : make a pause, edit message
  • edit : make a pause, make any changes
  • squash : commit squahed onto upper, message added up
  • fixup : commit squashed onto upper, message ignored
  • drop : do not add this commit

Merger conflicts appear often when you are trying to reorder changes to the same file. Good reason to keep entities/classes in their own files.

Result

Now your tree looks like

*    d034fee I' (HEAD -> fea01)
*    20f43d4 H'
*    35fcd3b G'
| *  c051b13 I4 (fea01-backup)
| *  0324a66 H3
| *  b23329b I3
| *  b91896e G4
| *  0b2fd60 I2
| *  f9f844d H2
| *  fbff5ee I1
| *  5f382e6 G3
| *  01664cf G2
| *  13ee55b H1
| *  ede1d37 G1
|/
*    acc1c91 F' (origin/dev, dev)
*    0b160f5 E
| *  e58c3c0 D (origin/master, master)
* |  801f263 C
|/
*    bdef04a B
*    9fa8598 A
*    6b2487d Init

You've got 3 squashed and ordered commits G', H', and I'. fea01-backup points at rebase source commits.

If anything goes wrong just get rid of rebased commits:

  1. git checkout fea01-backup - switch to backup branch
  2. git branch -f fea01 - forcedly re-assign fea01 to the "old" location

Now your're totally back. Try again :)

If you're happy with the rebase result (e.g. tests run OK) just get rid of fea01-backup.

# forcedly remove branch
git branch -D fea01-backup

# results in
*    d034fee I' (HEAD -> fea01)
*    20f43d4 H'
*    35fcd3b G'
*    acc1c91 F' (origin/dev, dev)
*    0b160f5 E
| *  e58c3c0 D (origin/master, master)
* |  801f263 C
|/
*    bdef04a B
*    9fa8598 A
*    6b2487d Init

Conflict with a remote

Pushing squashed commits

If commits you squashed from have been already pushed to remote then pushing squashed commits with git push origin fea01 will result in rejection:

 ! [rejected]        fea01 -> fea01 (non-fast-forward)
error: failed to push some refs to 'https://github.com/OleksiyRudenko/merge-google-slides.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Just force it with git push origin +fea01

Why not --force? To be sure a single ref will be updated forcedly. Check this resource for more details.

Peer developers

NB! Other contributors may be confused with such a significant repo history update. Limit yourself to squashing non-pushed commits only whenever possible.

So, you've updated remote but others may still have older commits on their local repos and git pull will (at best) raise conflicts or just rebase freshly squashed commits from remote on top of local git history and next push will result in remote having both old commits and squashed.

Here's what to do:

...<commands sequence to be completed yet>...

Workflow

You may keep developing on fea01 further, rebasing onto fea0-release and further onto dev from time to time whenever you have your changes consistent and wish to test it against codebase of a higher level.

So general workflow recurring feature development is:

  1. Branch off feaXX from dev
  2. Commit to feaXX
  3. Make a backup branch pointer feaXX-backup
  4. Rebase and test until you're happy
  5. Kill feaXX-backup
  6. ........ push..... ...<commands sequence to be completed yet>...
  7. ........ other users: ..... ...<commands sequence to be completed yet>...

Bonus

* 1b9180 Fix query further
| ...
* d4f67a Fix filtered query

1b9180 is a catch up of d4f67a where we've done a minor change (e.g. added a semi-colon)

Normally, you would squash it during rebase.

Just tell git to suggest squashing at interactive rebase when committing:

* 1b9180 fixup! Fix filtered query
| ...
* d4f67a Fix filtered query

At git rebase -i the commit SHA 1b9180 will be suggested right after d4f67a with a fixup command. Also git rebase -i --autosquash <targetBase> will try its best.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment