First a word of warning. Rebasing changes history, and should not be done on work that has been shared with others. But that doesn't mean it shouldn't be done. It's like proofing your work, and making it clean before you share it. Good professional practice.
And a technical point, especially for Windows users. This command uses a text editor as a point of interaction. The Notepad editor that ships with Windows 7 and earlier (and maybe later ones, I haven't checked), garbles the newlines that the Git rebase engine produces, because Git uses Unix style new-lines. Most recent editors (Notepad++, Notepad2, VS Code) can handle this, and if you use Git Extensions (which I recommend), then your Git settings will be set up to use the Git Extensions editor. You can check whether you have an editor set up by typing:
>git config --global core.editor
Since I have Git-Extensions installed, I get back this:
"C:/Program Files (x86)/GitExtensions/GitExtensions.exe" fileeditor
If you want to use Notepad++, for example, you can type in this (options included to make this function as a standalone interaction point, courtesy of this answer on Stack Overflow: http://stackoverflow.com/a/2486342/402949, also see http://docs.notepad-plus-plus.org/index.php/Command_Line_Switches)
>git config --global core.editor "'C:/Program Files (x86)/Notepad++/notepad++.exe' -multiInst -noplugin -nosesssion -notabbar"
Now lets' get a feel for using interactive rebase. Let's create a repo:
>git init testing
And let's add a file:
>echo file 1 content > file1.txt
And let's add that to the repo and commit it.
>git add file1.txt
>git commit -m "Add file1.txt"
Now let's create another couple of commits....
>echo file 2 content>file2.txt
>git add file2.txt
>git commit -m "Add file2.txt"
>echo file 3 content>file3.txt
>git add file3.txt
>git commit -m "Add file3.txt"
Okay, if you type git log now, you should see something like this:
C:\Users\Dan.Solovay\testing>git log
commit 1462505217add0edfe0451a2f608cbcf72cfbda2
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 18:00:48 2015 -0500
Add file3.txt
commit 3ad7253ba59e60642af2b0610a9e40c6af4b9f60
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 17:59:21 2015 -0500
Add file2.txt
commit 78f189fe8f38d80a58ce061e7b43a810674c55df
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 17:58:32 2015 -0500
Add file1.txt
And let use a more arcane command, but a really good one to know about...
>git reflog
1462505 HEAD@{0}: commit: Add file3.txt
3ad7253 HEAD@{1}: commit: Add file2.txt
78f189f HEAD@{2}: commit (initial): Add file1.txt
Pretty much the same information, presented a little differently. That will soon change.
Let's kick off an interactive rebase. The nature of the rebase command is that you rebase onto a commit, so we have to leave the initial commit alone, as our building block. We can launch the interactive rebase two ways:
git rebase 78f189f -i (You will need to change this to your initial commit.)
Or this:
git rebase "HEAD^^" -i
HEAD means most recent commit, HEAD^ is it's parent, and HEAD^^ is thus your initial commit. But.... the caret character means line continuation on Windows, so you need to but "HEAD^^" in quotes or you will get a "More?" prompt. You need to remember this when you read Git documentation and use it on Windows (cmd or PowerShell)..
Either command should cause Notepad++ to open, with this display:
pick 3ad7253 Add file2.txt
pick 1462505 Add file3.txt
# Rebase 78f189f..1462505 onto 78f189f
#
# 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
# x, exec = run command (the rest of the line) using shell
#
# 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
pick 1462505 Add file3.txt
pick 3ad7253 Add file2.txt
# Rebase 78f189f..1462505 onto 78f189f
#
# 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
# x, exec = run command (the rest of the line) using shell
#
# 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
Successfully rebased and updated refs/heads/master.
If you don't, you can always get back to your pre rebase state with this command: git rebase --abort
Now lets look at the log:
>git log
commit 61a380892db71cc02db305492867830059c2df22
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 17:59:21 2015 -0500
Add file2.txt
commit 527a0483703d281628e7f847aec7ecf7ef6babbe
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 18:00:48 2015 -0500
Add file3.txt
commit 78f189fe8f38d80a58ce061e7b43a810674c55df
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 17:58:32 2015 -0500
Add file1.txt
>git reflog
61a3808 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
61a3808 HEAD@{1}: rebase -i (pick): Add file2.txt
527a048 HEAD@{2}: rebase -i (pick): Add file3.txt
78f189f HEAD@{3}: rebase -i (start): checkout HEAD^^
1462505 HEAD@{4}: rebase -i (finish): returning to refs/heads/master
1462505 HEAD@{5}: rebase -i (start): checkout HEAD^^
1462505 HEAD@{6}: commit: Add file3.txt
3ad7253 HEAD@{7}: commit: Add file2.txt
78f189f HEAD@{8}: commit (initial): Add file1.txt
Actually, you won't have HEAD@{4} and HEAD@{5}, since those are only there because I forgot to save the first time I tried this. Reflog remembers everything, at least for 90 days. This makes it a very good place to look if you ever can't find a commit (e.g. due to a reset error).
Note the distinction between HEAD^^ (two commits back on this branch) and HEAD@{2} two commits back on this repo on this machine.
We can get back to our pre-rebase state by putting this in another branch:
git branch the-old-master 146205
git checkout the-old-master
git log
Now you will see the original history.
Or, you can create a branch based on the state after you added file3.txt, and before you added file2.txt. Of course, that moment never actually happened, but you photoshopped it into reality with the rebase:
git checkout master
git branch fake_moment "HEAD^"
git checkout fake_moment
dir
12/29/2015 06:34 PM <DIR> .
12/29/2015 06:34 PM <DIR> ..
12/29/2015 05:58 PM 14 file1.txt
12/29/2015 06:33 PM 16 file3.txt
2 File(s) 30 bytes
And you can go back to how things were by going back to master:
git checkout master
dir
12/29/2015 06:36 PM <DIR> .
12/29/2015 06:36 PM <DIR> ..
12/29/2015 05:58 PM 14 file1.txt
12/29/2015 06:36 PM 16 file2.txt
12/29/2015 06:33 PM 16 file3.txt
3 File(s) 46 bytes
Okay, let's do a little more. Let's combine the last two commits:
git rebase -i "HEAD^^"
And let's change the second commit to a "squash":
pick 527a048 Add file3.txt
s 61a3808 Add file2.txt
# Rebase 78f189f..61a3808 onto 78f189f
#
# 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
# x, exec = run command (the rest of the line) using shell
#
# 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
You'll get a chance to edit the combined message:
# This is a combination of 2 commits.
# The first commit's message is:
Add file3.txt
# This is the 2nd commit message:
Add file2.txt
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# rebase in progress; onto 78f189f
# You are currently editing a commit while rebasing branch 'master' on '78f189f'.
#
# Changes to be committed:
# new file: file2.txt
# new file: file3.txt
#
Let's accept by saving and closing. We can see the most recent commit with git show and see the combined message and the combined edits:
>git show
commit 424c80411552e87d1af316e06977e681bdec92a8
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 18:00:48 2015 -0500
Add file3.txt
Add file2.txt
commit 78f189fe8f38d80a58ce061e7b43a810674c55df
Author: dsolovay <dsolovay@gmail.com>
Date: Tue Dec 29 17:58:32 2015 -0500
Add file1.txt
A few other things you can do:
"f" works just like "s", but doesn't change the first commits message, and doesn't give you a chance to change it. You can also simply remove a commit to remove it from the branch history (including whatever file system changes you may have made.
This may sound cumbersome, but it really gets to be a groove, and allows you to contribute really well crafted commits with very little effort. Running interactive rebase starts to feel like giving an email a quick read before hitting "Send". Here is a commit of mine that was originally about 10 or so micro commits: https://github.com/dsolovay/hexo-migrator-rss/commit/5c44479c5027b9f45f39ec188c354df0baecc650, adding tests to a Node.js module. Having the commits grouped together with a detailed commit message makes it much easier for the person reviewing the pull request to think through.
And even if you never use this technique in your day-to-day coding, I recommend stepping through it at least once or twice with a test repo. It really helps anchor the concepts of local commits vs. upstream commits, and gives you a feeling for the power and flexibility that git offers you as an author of code.
No comments:
Post a Comment