git rebase

git rebase

I would like to add one post about git. Tool I've been using for several years. To be honest this is a standard nowadays in Dev and DevOps worlds. I was wondering for a while about the subject of the post and decided finally to pick up rebase. At the beginning I've used just few git command like add, commit, push and pull and I felt like a boss. Version control - level high. That time is far away but I remember that during my journey I had to spent some time to fully understand the power of git rebase.

Linear history: git rebase - basics

What is really important in the coding in distributed environment? The same what is really important in the troubleshooting in distributed environment where asynchronously many parties push the code to single repository. Here comes to the rescue workflows however KISS principle applies too. You can ask: what does it mean? Linear history! With that approach we can easier understand what is going on and easier troubleshoot the issues. Git rebase keeps the history more linear. This is fact.

Head to the project I've created small application with two branches

git init
echo m1 > m1 && git add . && git ci -m "m1"
echo m2 > m2 && git add . && git ci -m "m2"
git co -b feature
echo f1 > f1 && git add . && git ci -m "f1"
echo f2 > f2 && git add . && git ci -m "f2"
git co master
echo m3 > m3 && git add . && git ci -m "m3"

History of the git commits looks like that (and in all examples here the commits tree will be similar)

$ git log --graph --oneline --all
* efab315 (HEAD -> master) m3
| * 388b6a3 (feature) f2
| * 6d7a05d f1
|/  
* 772c03a m2
* e091294 m1

This is the a perfect example where rebase can help to create linear history. To do that we have to switch to feature branch and run following command:

git co feature
git rebase master

Simple isn't it? Let's look what we've got.

We've rewritten the history. We forced git to push (rebase) branch feature to the end of brach master. Historically we checkout from commit m2, but now current history shows that feature branch was created from commit m3. Kind a magic - but this is the main purpose of using rebase command.

The structure is almost linear already however master branch is still no in the right place. What's left is to merge it.

x
Updating efab315..d3dbe99
Fast-forward
f1 | 1 +
f2 | 1 +
2 files changed, 2 insertions(+)
create mode 100644 f1
create mode 100644 f2

Finally we got the linear structure easy to read, easy to understand and easy to troubleshoot

$ git log --graph --oneline --all
* d3dbe99 (HEAD -> master, feature) f2
* f6ca0ce f1
* efab315 m3
* 772c03a m2
* e091294 m1

Interactive git rebase

In addition to the linearity rebase in interactive mode offers many more fancy features. Let take a look at the similar project. In case of interactive mode we can manipulate commits before rebase. All these modification take place in th editor of your choice so be careful and set EDITOR environment variable before you start.

export EDITOR=vim

Rename

The history of commits looks familiar.

$ git log --graph --oneline --all
* e202a13 (HEAD -> feature) f5
* cf0f14b f4
* 00328eb f3
* 8add7f2 f2
* 1417c6f f1
| * 50ac919 (master) m3
|/  
* 772c03a m2
* e091294 m1

Starting set of commands

$ git co feature
$ git rebase master -i

And without warning we are jumping to the editor to decide what to to with given lists of commits. Here we are would like to change the message of one commit. Many time I made some typo of forget about JIRA info. That things happens and this is a way to fix that issues.

To change message in the commit toy have to add reword command before commit. (pick - do nothing like in non interactive version )

pick fee1b94 f1
reword ea594f9 f2
pick 15981d0 f3
pick d17db65 f4
pick 2dcb631 f5

# Rebase 50ac919..2dcb631 onto d17db65 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label

Once we save it we are being moved without any warning (again) to the editor to enter new message for the commit. As you can see git wasn't mistaken f2 commit is on the table.

f2_new

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Mon Feb 7 19:30:46 2022 +0100
#
# interactive rebase in progress; onto 50ac919
# Last commands done (2 commands done):
#    pick fee1b94 f1
#    reword ea594f9 f2
# Next commands to do (3 remaining commands):
#    pick 15981d0 f3
#    pick d17db65 f4
# You are currently editing a commit while rebasing branch 'feature' on '50ac919'.
#
# Changes to be committed:
#       new file:   f2
#

After saving it we got linear history with small adjustment we provided during interactive rebasing.

$ git log --graph --oneline --all
* c2ba713 (HEAD -> feature) f5
* 4055101 f4
* 9b1d812 f3
* dfa5057 f2_new
* fee1b94 f1
* 50ac919 (master) m3
* 772c03a m2
* e091294 m1

No pain here I guess.

Squash

Another useful options of interactive rebase is squashing. This is merging to separate commits info one with some message. Let's dive in. As a staring point again we have simple application of to branches.

$ git log --graph --oneline --all
* 20f7bd0 (HEAD -> master) m4
| * 2dcb631 (feature) f5
| * d17db65 f4
| * 15981d0 f3
| * ea594f9 f2
| * fee1b94 f1
|/  
* 50ac919 m3
* 772c03a m2
* e091294 m1

Situation is similar to the previous one in one exception instead reword I use squash command

$ git co feature
$ git rebase master -i

pick fee1b94 f1
squash ea594f9 f2
pick 15981d0 f3
pick d17db65 f4
pick 2dcb631 f5

# Rebase 20f7bd0..2dcb631 onto d17db65 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Obviously git jumps from rebase command select to setting of the commit message. Look at the beginning of the file "This is a combination of 2 commits". Which is what could be expected.

# This is a combination of 2 commits.
# This is the 1st commit message:

f1

# This is the commit message #2:

f2

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Mon Feb 7 19:30:39 2022 +0100
#
# interactive rebase in progress; onto 20f7bd0
# Last commands done (2 commands done):
#    pick fee1b94 f1
#    squash ea594f9 f2
# Next commands to do (3 remaining commands):
#    pick 15981d0 f3
#    pick d17db65 f4
# You are currently rebasing branch 'feature' on '20f7bd0'.
#
# Changes to be committed:
#       new file:   f1
#       new file:   f2
#

After applying changes the history of commits look as follows

$ git log --graph --oneline --all
* 8bd39d8 (HEAD -> feature) f5
* 4a85805 f4
* 79f0e18 f3
* 1fa76d1 f1
* 20f7bd0 (master) m4
* 50ac919 m3
* 772c03a m2
* e091294 m1

Wait a minute! Where is f2 commit? We lost commit bo not changes made by this commit. All is safe saved in f1 commit. Exactly as we wanted.

$ git log  | grep -v Author
commit 8bd39d834f7e65e97e7f3f9692f270794bb827ad
Date:   Mon Feb 7 19:33:05 2022 +0100

    f5

commit 4a85805e1ff9aa7b29c71b992ce3c48ebe532df1
Date:   Mon Feb 7 19:32:58 2022 +0100

    f4

commit 79f0e183315f12593df04caaa02004361b2efe82
Date:   Mon Feb 7 19:32:47 2022 +0100

    f3

commit 1fa76d1a7349f9612da81370280d10f34d3bdea3
Date:   Mon Feb 7 19:30:39 2022 +0100

    f1
    
    f2

commit 20f7bd0e6731d0cab76f4c142d4dc7c1c5dfbe01
Date:   Mon Feb 7 20:16:58 2022 +0100

    m4

Fixup

Similar behaviour to squash has fixup command. There is one difference: lack of stage for entering commit message. Please have a look. Simple code.

$ git log --graph --oneline --all
* f89f551 (HEAD -> feature) f4
* 2fa5b07 f3
* 03c6276 f2
* 37a80d6 f1
| * a5f75f0 (master) m3
|/  
* 16137ae m2
* a973b7a m1

Standard rebase interactive initiation and fixup command entered.

$ git co feature 
$ git rebase master -i

pick 37a80d6 f1
fixup 03c6276 f2
pick 2fa5b07 f3
pick f89f551 f4

# Rebase a5f75f0..f89f551 onto f89f551 (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.

Once you save to jump to the terminal. There is no commit message state.

$ git log --graph --oneline --all
* 2f15320 (HEAD -> feature) f4
* c3516b2 f3
* c3dc225 f1
* a5f75f0 (master) m3
* 16137ae m2
* a973b7a m1

To make sure that all change are still in place we can perform diff command in f1 commit

$ git diff c3dc225~ c3dc225
diff --git a/f1 b/f1
new file mode 100644
index 0000000..9dd7ac9
--- /dev/null
+++ b/f1
@@ -0,0 +1 @@
+f1
\ No newline at end of file
diff --git a/f2 b/f2
new file mode 100644
index 0000000..70f80fc
--- /dev/null
+++ b/f2
@@ -0,0 +1 @@
+f2
\ No newline at end of file

Simple, isn't it?

There are more options available int rebase interactive like drop but let me finish here on that level.

Conclusion

At the beginning of my journey with code I've used only few commands. And it was ok. But to tell the truth those projects were quite simple and small that's why limited usage of git was good enough. When you going deep you have to keep the project as simple as possible. Git rebase at first glance is complex however I encourage you to give a try. I believe examples I've show you demystified a bit the git rebase.

Leave a Reply

Your email address will not be published. Required fields are marked *