The tutorial is performed as a test task which shows how to build patch series in form multiple independent clear atomic commits, and most interestingly - how to work with such a patchset: change commit order, add new commits, drop not needed ones, change commit messages, modify code in the middle of the series, and all sorts of other changes. All that while not requiring to redo the patchset from scratch.
Small atomic commits forming a story-like patch series are 1) actually quite easy to make after some training, 2) very easy to review step by step, 3) good for git history of your repo.
This tutorial is only about the linear patchset structure. It doesn't involve any work with merge-commits. The linear patchset is typically about a certain bugfix or a feature and doesn't contain any merge commits. These can appear in the main/master branch when the sub-branches are being merged into the main stream, but they are not supposed to appear in the sub-branches as a part of your own patchset which is in work yet.
These are the commands which are needed for working on a patchset.
-
git stash
- all the current not-committed changes can be saved into a temporary storage without creating any commits, and then applied at any other moment. Essentially, it saves thegit diff
and discards the changes. Thengit stash apply/pop
applies the saved diff. -
git commit --amend
- merge the current changes into the current head-commit instead of creating a new one. This is very useful when working on a next commit and doing it in small parts when all should in the end become one commit. -
git reset [<hash>]
- soft reset to the given commit will remove all the commits on top of this one while keeping their changes. The changes won't disappear. Will be simply "not staged for commit" again. By default the hash isHEAD
. Then it simply makes all "to be committed" changes "not staged for commit" again. -
git reset --hard [<hash>]
- hard reset will delete all the commits on top of this one with all their changes. It is very useful when did some work and decided to discard it all at once, or want to get rid of some latest commits. By default it will delete all changes, both staged and just modified. Only untracked files remain intact. -
git rebase -i <hash>
- this is the main command for building patchsets. It is used for re-applying all the commits of this branch on top of the given hash or branch name. Which makes it the tool for refreshing your branch on top of the latest main/master/develop, and for rebasing the branch on itself to change some commits in the middle of it. -
git blame <file>
- for a given file it prints its content with commit hash and author name near each line showing when it was changed last time. It is very helpful to find why something was done, or when was a bug introduced. If the culprit is not the last commit changing this line, then can jump to the commit before this one, and makeblame
again to find the previous one, and so on. Sadly, in console it might be complicated to do all these jumps. Easiest way is in the repository's web-page, where theseblame->commit->prev_commit->blame->...
steps can be done very quick.
This is all not counting the very basic commands like plain git commit
, git add
, etc, which are still needed and useful, and are too simply to consider them all here individually.
This repository offers a task which can help one to use all these commands from above.
There are modules:
core
- the basic functionality used by everything.bar
- a library offeringstruct ObjectBar
and its stuff.foo
- a library offeringstruct ObjectFoo
and also stuff.a.out
- the executable which uses all these libraries.
This application is already fully implemented in a clean patch series on the branch master
.
However the original draft implementation of this all was a dirty branch source
which implements the same, but with some problems:
- Some debug prints are not deleted.
- Certain commits are not atomic, and others are too highly granulated.
- Commit order is not good.
- Not all code is needed.
Lets compare these branches code-wise.
git checkout source
git diff master
Diff output
diff --git a/core.cpp b/core.cpp
index 2a5d3d1..117a4e3 100644
--- a/core.cpp
+++ b/core.cpp
@@ -1,5 +1,7 @@
#include "core.h"
+#include <iostream>
+
static std::string_view keys[] = {
"Kez",
"Kex",
@@ -35,15 +37,19 @@ FlibberCollection::flibber()
std::string
FlibberCollection::toString(uint8_t tabs) const
{
+ std::cout << "----------------------------- toString1\n";
if (myFlibbers.size() == 0)
- return {};
+ return "<empty>\n";
+
+ std::cout << "----------------------------- toString2\n";
std::string tab;
for (uint8_t i = 0; i < tabs; ++i)
tab += '\t';
- std::string res = tab + "flibbers:\n";
- for (const Flibber& fli : myFlibbers)
- res += tab + '\t' + fli.myKey + ": " + fli.myValue + '\n';
+ std::cout << "----------------------------- toString3\n";
+ std::string res = tab + "flibbers: <PLACEHOLDER>\n";
+
+ std::cout << "----------------------------- toString4\n";
return res;
}
diff --git a/main.cpp b/main.cpp
index d79e1e2..69ae939 100644
--- a/main.cpp
+++ b/main.cpp
@@ -8,7 +8,7 @@ main()
{
std::cout << "#### Foo\n";
- ObjectFoo f = makeFoo();
+ ObjectFoo f = makeFoo(10);
std::cout << f.toString(1) << '\n';
std::cout << "# Perform wibble-zorbling\n";
diff --git a/moduleBar.h b/moduleBar.h
index 06b52b4..a28b63e 100644
--- a/moduleBar.h
+++ b/moduleBar.h
@@ -5,7 +5,7 @@
struct ObjectBar : public FlibberCollection
{
void splinx();
- void yibble() { myYibble = (myYibble + 1) * 2; }
+ void yibble() { myYibble = myYibble + 1; }
std::string toString(uint8_t tabs) const;
std::string mySplinx = "Emptio";
diff --git a/moduleFoo.cpp b/moduleFoo.cpp
index c94eb4c..bd2e17e 100644
--- a/moduleFoo.cpp
+++ b/moduleFoo.cpp
@@ -21,7 +21,9 @@ ObjectFoo::toString(uint8_t tabs) const
}
ObjectFoo
-makeFoo()
+makeFoo(int wibble)
{
- return ObjectFoo();
+ ObjectFoo f;
+ f.myWibble = wibble;
+ return f;
}
diff --git a/moduleFoo.h b/moduleFoo.h
index 35a971c..cdafafe 100644
--- a/moduleFoo.h
+++ b/moduleFoo.h
@@ -15,4 +15,4 @@ struct ObjectFoo : public FlibberCollection
char myZorble = 'a';
};
-ObjectFoo makeFoo();
+ObjectFoo makeFoo(int wibble);
These are easy to fix, but we need to do it clean. The changes should go into the commits where they belong. Lets see the git-log difference.
git log --oneline source
git log --oneline master
############################# Source #############################
hash "[wip] New splinxes for Bar"
hash "[wip] FlibberCollection debug"
hash "[wip] toString() for Foo and Bar"
hash "Introduce FlibberCollection"
hash "[wip] Unwibbling?"
hash "[wip] Flibber in foo"
hash "[wip] Flibber in bar"
hash "[wip] Flibber test"
hash "[wip] Introduce core library with flibber"
hash "Introduce bar module"
hash "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
############################# Master #############################
hash "FlibberCollection::toString()"
hash "Make Foo responsible for to-string"
hash "Make Bar responsible for to-string"
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
The source git log is quite dirty. It has to be cleaned and the code changes must be made in the appropriate commits. Otherwise review of this source
branch would be a nightmare. It simply won't be possible commit-by-commit, because the commits make little sense.
Lets summarize what has to be done.
- The commit
"[wip] New splinxes for Bar"
must be merged into"Introduce bar module"
. Apparently, in the latter we forgot these splinxes, added them later, but they really belong to the bar intro commit. - The commit
"[wip] FlibberCollection debug"
must be changed. It contains debug prints, a mistake, and its message is crap. - The commit
"[wip] Unwibbling?"
needs a new message. The commit itself is fine, but the message is bad. - The commits
"[wip] Flibber in foo"
,"[wip] Flibber in bar"
,"[wip] Flibber test"
,"[wip] Introduce core library with flibber"
must be merged (squashed). They are too granular. And tests are usually supposed to be in the same commit as code changes. - The commit
"[wip] Specify wibble in makeFoo()?"
must be deleted. ThemakeFoo()
argument isn't needed. - The commit
"[wip] toString() for Foo and Bar"
must be split into 2: for foo and bar separately. - The commit
"Introduce bar module"
must changeObjectBar::yibble()
frommyYibble = myYibble + 1;
tomyYibble = (myYibble + 1) * 2;
. - In the end lets inspect with
git-blame
the results.
Lets do them. You can also do the steps yourself when you are confident enough, to train more.
Before start, lets checkout to a new branch where we wouldn't ruin the original source
and would be able to get back to start if stuff is too broken.
git checkout -b new-source
Lets see the log: git log --oneline
.
hash "[wip] New splinxes for Bar" # ->--->--->--->---> Need to merge it here.
hash "[wip] FlibberCollection debug" # |
hash "[wip] toString() for Foo and Bar" # |
hash "Introduce FlibberCollection" # V
hash "[wip] Unwibbling?" # |
hash "[wip] Flibber in foo" # |
hash "[wip] Flibber in bar" # V
hash "[wip] Flibber test" # |
hash "[wip] Introduce core library with flibber" # |
# V
hash-A "Introduce bar module" # <---<---<---<---<---<---<---<---<--+
hash-B "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
Need to merge "[wip] New splinxes for Bar"
into "Introduce bar module"
. When doing any changes to older commits, you always need git rebase -i
. The argument -i
makes it interactive where before rebase you can do some tweaks.
Note, that when doing git rebase -i <hash>
, the commits older and including <hash>
will stay as is, and the newer ones are re-applied as you set them up to. Which means that if the oldest commit to change is <hash>
itself, then you need to rebase to <hash>~1
- previous one. Lets do that.
git rebase -i <hash-A>~1 # Or same as `git rebase -i <hash-B>`.
This will put your HEAD
to this state:
# <HEAD> <--- YOU ARE HERE
hash "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
And git will open a text editor of your choice (normally nano
or vim
) with the following content:
pick hash "Introduce bar module" # <---<---<---<---<---<+
pick hash "[wip] Introduce core library with flibber" # |
pick hash "[wip] Flibber test" # |
pick hash "[wip] Flibber in bar" # |
pick hash "[wip] Flibber in foo" # | Need to merge it.
pick hash "[wip] Unwibbling?" # |
pick hash "Introduce FlibberCollection" # |
pick hash "[wip] toString() for Foo and Bar" # |
pick hash "[wip] FlibberCollection debug" # |
pick hash "[wip] New splinxes for Bar" # --->--->--->--->
# ... some tutorial commands here, a cheat-sheet with what can be changed and how.
This is a command-list which will get executed after you save and close the editor, from top to bottom, on top of the commit which you specified in git rebase
command. Which is now our HEAD
.
In order to merge "[wip] New splinxes for Bar"
into "Introduce bar module"
lets move them to each other in the command list and replace pick
with fixup
or f
. The command fixup
instead of applying this commit as is will merge it into the previous (above in the list) commit.
pick hash "Introduce bar module"
# ^^^ fixup merges its commit into the previous one.
f hash "[wip] New splinxes for Bar" # <---<---<--- This was moved.
pick hash "[wip] Introduce core library with flibber"
pick hash "[wip] Flibber test"
pick hash "[wip] Flibber in bar"
pick hash "[wip] Flibber in foo"
pick hash "[wip] Unwibbling?"
pick hash "Introduce FlibberCollection"
pick hash "[wip] toString() for Foo and Bar"
pick hash "[wip] FlibberCollection debug"
Now save and exit the editor, and all these commands are applied.
# Successfully rebased and updated refs/heads/new-source.
$> git log --oneline
hash "[wip] FlibberCollection debug"
hash "[wip] toString() for Foo and Bar"
hash "Introduce FlibberCollection"
hash "[wip] Unwibbling?"
hash "[wip] Flibber in foo"
hash "[wip] Flibber in bar"
hash "[wip] Flibber test"
hash "[wip] Introduce core library with flibber"
hash "Introduce bar module"
hash "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
The commit "[wip] New splinxes for Bar"
was merged into "Introduce bar module"
. You can check that by looking at git show
of this commit.
The commit "[wip] FlibberCollection debug"
must be changed. It contains debug prints, a mistake, and its message is crap.
Lets see what is wrong with it.
git show # `HEAD` is default, and is the commit that we need to change.
Commit Data
commit fdd3c226c97efc2ab7456daa06ab7333418a537b (HEAD -> new-source)
Author: Vladislav Shpilevoy <[email protected]>
Date: Thu Apr 18 13:03:17 2024 +0200
[wip] FlibberCollection debug
diff --git a/core.cpp b/core.cpp
index f30ffbd..117a4e3 100644
--- a/core.cpp
+++ b/core.cpp
@@ -1,5 +1,7 @@
#include "core.h"
+#include <iostream>
+
static std::string_view keys[] = {
"Kez",
"Kex",
@@ -32,6 +34,25 @@ FlibberCollection::flibber()
myFlibbers.push_back(makeFlibber());
}
+std::string
+FlibberCollection::toString(uint8_t tabs) const
+{
+ std::cout << "----------------------------- toString1\n";
+ if (myFlibbers.size() == 0)
+ return "<empty>\n";
+
+ std::cout << "----------------------------- toString2\n";
+ std::string tab;
+ for (uint8_t i = 0; i < tabs; ++i)
+ tab += '\t';
+
+ std::cout << "----------------------------- toString3\n";
+ std::string res = tab + "flibbers: <PLACEHOLDER>\n";
+
+ std::cout << "----------------------------- toString4\n";
+ return res;
+}
...
...
It contains debug prints, missing functionality of printing the flibbers, and its message is bad. Lets fix the code first. It has to be done manually. And this is the diff:
Diff
diff --git a/core.cpp b/core.cpp
index 117a4e3..2a5d3d1 100644
--- a/core.cpp
+++ b/core.cpp
@@ -1,7 +1,5 @@
#include "core.h"
-#include <iostream>
-
static std::string_view keys[] = {
"Kez",
"Kex",
@@ -37,19 +35,15 @@ FlibberCollection::flibber()
std::string
FlibberCollection::toString(uint8_t tabs) const
{
- std::cout << "----------------------------- toString1\n";
if (myFlibbers.size() == 0)
- return "<empty>\n";
-
- std::cout << "----------------------------- toString2\n";
+ return {};
std::string tab;
for (uint8_t i = 0; i < tabs; ++i)
tab += '\t';
- std::cout << "----------------------------- toString3\n";
- std::string res = tab + "flibbers: <PLACEHOLDER>\n";
-
- std::cout << "----------------------------- toString4\n";
+ std::string res = tab + "flibbers:\n";
+ for (const Flibber& fli : myFlibbers)
+ res += tab + '\t' + fli.myKey + ": " + fli.myValue + '\n';
return res;
}
Lets merge it into the top commit:
git add -u # Stage all tracked changes.
git commit --amend # Merge them into the `HEAD` commit.
If you make git show
now, you will see the commit is changed. Now need to update the message. This is also the job for amend - git commit --amend
. And use the commit message same as in master
:
FlibberCollection::toString()
Prints the entire collection, good for code reuse.
Using git show
one can confirm that the message and commit diff were updated and are now just like in master
branch.
The commit "[wip] Unwibbling?"
needs a new message. The commit itself is fine, but the message is bad.
Lets see the log - git log --oneline
.
hash "FlibberCollection::toString()"
hash "[wip] toString() for Foo and Bar"
hash "Introduce FlibberCollection"
hash "[wip] Unwibbling?" # <---<---<---<---<---<--- Commit with bad message.
hash-A "[wip] Flibber in foo"
hash "[wip] Flibber in bar"
hash "[wip] Flibber test"
hash "[wip] Introduce core library with flibber"
hash "Introduce bar module"
hash "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
Need to update message of "[wip] Unwibbling?"
. This is the job for interactive rebase.
git rebase -i <hash-A>
The branch state now:
# <HEAD> <--- YOU ARE HERE
hash "[wip] Flibber in foo"
hash "[wip] Flibber in bar"
hash "[wip] Flibber test"
hash "[wip] Introduce core library with flibber"
hash "Introduce bar module"
hash "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
The prepared commands are:
pick hash "[wip] Unwibbling?" # <---<---<---<---<--- Need to change.
pick hash "Introduce FlibberCollection"
pick hash "[wip] toString() for Foo and Bar"
pick hash "FlibberCollection::toString()"
To change just the message and nothing else there is a special command reword
or r
. Lets use it:
r hash "[wip] Unwibbling?" # <---<---<---<---<--- Need to change.
pick hash "Introduce FlibberCollection"
pick hash "[wip] toString() for Foo and Bar"
pick hash "FlibberCollection::toString()"
Now after saving and exiting the editor the git applier will stop at this commit, open the editor again, and suggest to change the commit message. Lets use the same one as in master
:
Support unwibbling of Foo
Sometimes wibble is too big, need to be able to unwibble it.
After saving and exiting the editor, the other commands are execute silently, and we are on top of the branch again. Using git log
one can confirm that the message of the target commit was updated.
The commits "[wip] Flibber in foo"
, "[wip] Flibber in bar"
, "[wip] Flibber test"
, "[wip] Introduce core library with flibber"
must be merged (squashed). They are too granular. And tests are usually supposed to be in the same commit as code changes.
There are 4 commits which need to be squashed into one. Lets see the current state of the branch: git log --oneline
.
hash "FlibberCollection::toString()"
hash "[wip] toString() for Foo and Bar"
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash-A "[wip] Flibber in foo"
hash-B "[wip] Flibber in bar"
hash-C "[wip] Flibber test"
hash-D "[wip] Introduce core library with flibber"
hash-E "Introduce bar module"
hash "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
Need to squash hash-A
- hash-D
. This is the job for interactive rebase.
git rebase -i <hash-E>
The branch state now:
# <HEAD> <--- YOU ARE HERE
hash-E "Introduce bar module"
hash "[wip] Specify wibble in makeFoo()?"
hash "Initial commit"
The prepared commands are:
# --------------------------------------------------------+
pick hash "[wip] Introduce core library with flibber" # |
pick hash "[wip] Flibber test" # | To squash.
pick hash "[wip] Flibber in bar" # |
pick hash "[wip] Flibber in foo" # |
# --------------------------------------------------------+
pick hash "Support unwibbling of Foo"
pick hash "Introduce FlibberCollection"
pick hash "[wip] toString() for Foo and Bar"
pick hash "FlibberCollection::toString()"
Squash is the same as fixup, but it allows to change the commit message, which is right now necessary. squash
or s
command will merge the given commit into the previous one and open the editor to allow to type a new commit message. The commands have to be changed to this:
# --------------------------------------------------------+
pick hash "[wip] Introduce core library with flibber" # |
s hash "[wip] Flibber test" # | To squash.
s hash "[wip] Flibber in bar" # |
s hash "[wip] Flibber in foo" # |
# --------------------------------------------------------+
pick hash "Support unwibbling of Foo"
pick hash "Introduce FlibberCollection"
pick hash "[wip] toString() for Foo and Bar"
pick hash "FlibberCollection::toString()"
After saving and exiting the editor the git applier will merge these commits and open the editor again where a new message can be typed. Lets type the same one as in master
branch:
Add flibbering support
Now bar and foo are able to flibbe and maintain a queue of flibbers.
After saving and exiting the editor, the other commands are execute silently, and we are on top of the branch again. Using git log
one can confirm that the commits were squashed into one and the message is updated. To see the new commit's content use git show
on its hash.
The commit "[wip] Specify wibble in makeFoo()?"
must be deleted. The makeFoo()
argument isn't needed.
The mentioned commit isn't needed. Lets find it in git log --oneline
:
hash "FlibberCollection::toString()"
hash "[wip] toString() for Foo and Bar"
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "[wip] Specify wibble in makeFoo()?" # <---<---<--- To delete.
hash-A "Initial commit"
It is an old commit. Its deletion most likely will cause conflicts in newer commits. That would be a chance to try dealing with them. Lets rebase.
git rebase -i <hash-A>
The branch state now:
# <HEAD> <--- YOU ARE HERE
hash-A "Initial commit"
The prepared commands are:
pick hash "[wip] Specify wibble in makeFoo()?" # <---<---<---<---<--- To delete.
pick hash "Introduce bar module"
pick hash "Add flibbering support"
pick hash "Support unwibbling of Foo"
pick hash "Introduce FlibberCollection"
pick hash "[wip] toString() for Foo and Bar"
pick hash "FlibberCollection::toString()"
To delete a commit it is enough to just drop its line from the command list:
# Comment it out or delete - "[wip] Specify wibble in makeFoo()".
pick hash "Introduce bar module"
pick hash "Add flibbering support"
pick hash "Support unwibbling of Foo"
pick hash "Introduce FlibberCollection"
pick hash "[wip] toString() for Foo and Bar"
pick hash "FlibberCollection::toString()"
Lets save and exit the editor. Git applier will execute the remaining commands.
Unfortunately, not all commands could be applied. Git applier is stuck here:
Auto-merging main.cpp
CONFLICT (content): Merge conflict in main.cpp
Auto-merging moduleFoo.cpp
Auto-merging moduleFoo.h
error: could not apply <hash> [wip] toString() for Foo and Bar
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply <hash> [wip] toString() for Foo and Bar
Lets see the state: git status
.
interactive rebase in progress; onto <hash>
Last commands done (5 commands done):
pick <hash> Introduce FlibberCollection
pick <hash> [wip] toString() for Foo and Bar
(see more in file .git/rebase-merge/done)
Next command to do (1 remaining command):
pick <hash> FlibberCollection::toString()
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'new-source' on '<hash>'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: ../moduleBar.cpp
modified: ../moduleBar.h
modified: ../moduleFoo.cpp
modified: ../moduleFoo.h
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: ../main.cpp
It couldn't apply "[wip] toString() for Foo and Bar". the conflict is in both modified: ../main.cpp
.
The current position in the branch: git log --oneline
.
# <HEAD> <--- YOU ARE HERE
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
And the remaining commands are:
pick hash "[wip] toString() for Foo and Bar" # <---<---<--- Current one, broken.
pick hash "FlibberCollection::toString()" # <---<---<---<-- Next one.
The conflicts must be fixed. Lets have a look. Open the file main.cpp
and see this:
int
main()
{
std::cout << "#### Foo\n";
<<<<<<< HEAD
ObjectFoo f = makeFoo();
std::cout << "\tFoo's wibble: " << f.myWibble << '\n';
std::cout << "\tFoo's zorble: " << f.myZorble << '\n';
std::cout << '\n';
=======
ObjectFoo f = makeFoo(10);
std::cout << f.toString(1) << '\n';
>>>>>>> 285b6d6 ([wip] toString() for Foo and Bar)
The stuff between <<<<<<< ... =======
is what is in HEAD
, before the failed command.
The stuff between ======= ... >>>>>>>
is what the failed command wanted to do, but couldn't.
Most of the time you delete one of those versions fully and keep the other. Sometimes can join them into something new.
This time need to keep the new version, but drop the makeFoo()
argument:
int
main()
{
std::cout << "#### Foo\n";
ObjectFoo f = makeFoo();
std::cout << f.toString(1) << '\n';
Now save main.cpp
, check that the build works, and a.out
runs fine. And finish the failed pick
command.
git add -u # Stage all modified files, including conflicted ones.
git rebase --continue # Continue the rebase. The failed command is finished with
# the updated changes. And next commands are being executed.
See in git log
that the commit is gone. Also check that its code is gone as well.
It contains changes which are completely independent, even though they are similar. Lets split them into separate commits. See the log git log --oneline
:
hash "FlibberCollection::toString()"
hash-A "[wip] toString() for Foo and Bar" # <---<---<---< To split.
hash-B "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
This is the job for rebase.
git rebase -i <hash-B>
The branch state now:
# <HEAD> <--- YOU ARE HERE
hash-B "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
The prepared commands are:
pick b5d3551 "[wip] toString() for Foo and Bar" # <---<---<--- To split.
pick a17e969 "FlibberCollection::toString()"
Splitting means commit editing. Lets change pick
to edit
or e
.
e b5d3551 "[wip] toString() for Foo and Bar" # <---<---<--- To split.
pick a17e969 "FlibberCollection::toString()"
After saving and exiting the editor, the git applier stops at the edit
command. Now this is the state:
# <HEAD> <--- YOU ARE HERE
hash-A "[wip] toString() for Foo and Bar" # <---<---<---< To split.
hash-B "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
Editing can include anything. Can amend new changes, change commit, title, etc. In the current case this commit has to be split in 2. Firstly, lets drop it while keeping its changes.
git reset HEAD~1
This is the current state:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ../main.cpp
modified: ../moduleBar.cpp
modified: ../moduleBar.h
modified: ../moduleFoo.cpp
modified: ../moduleFoo.h
# <HEAD> <--- YOU ARE HERE
hash-B "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
The changes of the commit to split are preserved as "not staged". The commit is gone now. Lets commit these changes in 2 separate commits. One for foo
and another for bar
. It would be easy to just git add
the foo
files, then commit, then add bar
files, then also commit. But main.cpp
has changes needed for both commits. How exactly to split it 2 is out of scope of this project. But the easiest way is to use git add --patch
. It allows to interactively select individual git-diff hunks to stage for commit, and even allows their editing before staging.
This staging is done behind the scenes using git add --patch
. Bar
changes are added, lets see the state git status
:
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: ../main.cpp
modified: ../moduleBar.cpp
modified: ../moduleBar.h
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ../main.cpp
modified: ../moduleFoo.cpp
modified: ../moduleFoo.h
Now can commit the bar
stuff like in master
, with the given message, using git commit
:
Make Bar responsible for to-string
Makes it easier to reuse the serialization.
Lets see the new state, git status
+ git log --oneline
:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ../main.cpp
modified: ../moduleFoo.cpp
modified: ../moduleFoo.h
# <HEAD> <--- YOU ARE HERE
hash "Make Bar responsible for to-string" # <---<---<---<--- New commit.
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
Lets check if the new commit isn't broken without these unstaged changes.
- Stash the changes using
git stash
. - Try to build and run the code.
- If any bugs, then fix them and amend into the current head.
- When done, unstash the
foo
changes withgit stash pop
.
Now can commit the foo changes with the message from master
:
Make Foo responsible for to-string
Makes it easier to reuse the serialization.
The current state, git log --oneline
:
# <HEAD> <--- YOU ARE HERE
hash "Make Foo responsible for to-string" # <---<---<---<--- New commit.
hash "Make Bar responsible for to-string" # <---<---<---<--- New commit.
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
The new commits are here. Now lets continue the rebase to apply the next commits, git rebase --continue
. All done now.
See in git log
that the new commits took place of the old one.
The commit "Introduce bar module"
must change ObjectBar::yibble()
from myYibble = myYibble + 1;
to myYibble = (myYibble + 1) * 2;
.
Lets find the target commit in git log --oneline
:
hash "FlibberCollection::toString()"
hash "Make Foo responsible for to-string"
hash "Make Bar responsible for to-string"
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash-A "Introduce bar module" # <---<---<---<--- To edit.
hash-B "Initial commit"
In order to change this commit we need to rebase from below it:
git rebase -i <hash-B>
This is the current state:
# <HEAD> <--- YOU ARE HERE
hash-B "Initial commit"
and these commands are going to be applied:
pick hash "Introduce bar module" # <---<---<---<--- To change.
pick hash "Add flibbering support"
pick hash "Support unwibbling of Foo"
pick hash "Introduce FlibberCollection"
pick hash "Make Bar responsible for to-string"
pick hash "Make Foo responsible for to-string"
pick hash "FlibberCollection::toString()"
Lets change pick
to e
in the first line to edit the first commit:
e hash "Introduce bar module" # <---<---<---<--- To change.
After saving and exiting the editor, this is the current state:
# <HEAD> <--- YOU ARE HERE
hash-A "Introduce bar module"
hash-B "Initial commit"
Lets change the needed place manually:
diff --git a/moduleBar.h b/moduleBar.h
index c4afca5..2b7e6c1 100644
--- a/moduleBar.h
+++ b/moduleBar.h
@@ -5,7 +5,7 @@
struct ObjectBar
{
void splinx();
- void yibble() { myYibble = myYibble + 1; }
+ void yibble() { myYibble = (myYibble + 1) * 2; }
std::string mySplinx = "Emptio";
uint64_t myYibble = 0;
And commit it into HEAD
using git commit -a --amend --no-edit
. The last option is to keep the old commit message as is.
Now lets continue with applying the next commands, git rebase --continue
.
Unfortunately, one the next commits couldn't be pick
ed so easily, there is a conflict:
Auto-merging moduleBar.h
CONFLICT (content): Merge conflict in moduleBar.h
error: could not apply <hash> Add flibbering support
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply <hash> Add flibbering support
It was the next command, so git log --oneline
looks like before. The next commit failed to apply and needs to be fixed. Lets see the conflict:
struct ObjectBar
{
void splinx();
<<<<<<< HEAD
void yibble() { myYibble = (myYibble + 1) * 2; }
=======
void yibble() { myYibble = myYibble + 1; }
void flibber();
>>>>>>> 7369c88 (Add flibbering support)
We should keep the new yibble()
+ the flibber()
from the failed commit. Lets edit it to this:
struct ObjectBar
{
void splinx();
void yibble() { myYibble = (myYibble + 1) * 2; }
void flibber();
Now compile and run the project to ensure that it works, then stage the new code and continue the rebase:
git add -u
git rebase --continue
Another conflict, lets see:
Auto-merging moduleBar.h
CONFLICT (content): Merge conflict in moduleBar.h
error: could not apply <hash> Introduce FlibberCollection
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply <hash> Introduce FlibberCollection
The state is:
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: ../core.cpp
modified: ../core.h
modified: ../moduleBar.cpp
modified: ../moduleFoo.cpp
modified: ../moduleFoo.h
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: ../moduleBar.h
# <HEAD> <--- YOU ARE HERE
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
The changes belong to the next commit - "Introduce FlibberCollection"
. Lets see the conflict:
struct ObjectBar : public FlibberCollection
{
void splinx();
<<<<<<< HEAD
void yibble() { myYibble = (myYibble + 1) * 2; }
void flibber();
=======
void yibble() { myYibble = myYibble + 1; }
>>>>>>> 1efcbdf (Introduce FlibberCollection)
Need to keep the new yibble()
and drop flibber()
like in this new commit. It was moved into FlibberCollection
. This is the updated code:
struct ObjectBar : public FlibberCollection
{
void splinx();
void yibble() { myYibble = (myYibble + 1) * 2; }
Now compile and run the project to ensure that it works, then stage the new code and continue the rebase:
git add -u
git rebase --continue
Oh no, another conflict!
Auto-merging moduleBar.h
CONFLICT (content): Merge conflict in moduleBar.h
error: could not apply <hash> Make Bar responsible for to-string
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply <hash> Make Bar responsible for to-string
Lets see the state:
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: ../main.cpp
modified: ../moduleBar.cpp
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: ../moduleBar.h
# <HEAD> <--- YOU ARE HERE
hash "Introduce FlibberCollection"
hash "Support unwibbling of Foo"
hash "Add flibbering support"
hash "Introduce bar module"
hash "Initial commit"
The changes belong to the next commit - "Make Bar responsible for to-string"
. Lets see the conflict:
struct ObjectBar : public FlibberCollection
{
void splinx();
<<<<<<< HEAD
void yibble() { myYibble = (myYibble + 1) * 2; }
=======
void yibble() { myYibble = myYibble + 1; }
std::string toString(uint8_t tabs) const;
>>>>>>> 3bddb0e (Make Bar responsible for to-string)
Need to keep the new yibble()
and the toString()
from the new commit. This is the updated code:
struct ObjectBar : public FlibberCollection
{
void splinx();
void yibble() { myYibble = (myYibble + 1) * 2; }
std::string toString(uint8_t tabs) const;
Now compile and run the project to ensure that it works, then stage the new code and continue the rebase:
git add -u
git rebase --continue
This one reaches the end of the command list without more conflicts and the rebase is finished. Now you can check that the change is done throughout the whole git log like if it was always there, and there is no trace of the old version of yibble()
.
Now lets confirm that the specific line that was changed in the previous step was indeed done in one of the first commits, and wasn't touched later.
git blame -s moduleBar.h # `-s` is a shorter output.
hash-A 1) #pragma once
hash-A 2)
hash-B 3) #include "core.h"
hash-B 4)
hash-C 5) struct ObjectBar : public FlibberCollection
hash-A 6) {
hash-A 7) void splinx();
hash-A 8) void yibble() { myYibble = (myYibble + 1) * 2; }
hash-D 9) std::string toString(uint8_t tabs) const;
hash-A 10)
hash-A 11) std::string mySplinx = "Emptio";
hash-A 12) uint64_t myYibble = 0;
hash-A 13) };
hash-A 14)
hash-A 15) ObjectBar makeBar();
Can see that yibble()
was last time changed in commit hash-A
. Lets see which one is that:
git show <hash-A>
commit hash-A
Author: Vladislav Shpilevoy <[email protected]>
Date: <date>
Introduce bar module
It is able both to splinx and yibble. While for wibble-zorbling the foo
module is responsible.
And apparently it is the commit which was edited on the previous step. All worked as expected.
Now when the patchset is done, it must be the same as the one at master
branch. Check the code using git diff master
while being on your branch. The diff must be empty. Then compare the individual commits.
The branch can be revisited commit-by-commit one last time to check and maybe fix some commit messages and to see if each of them is buildable and runnable. That would make them atomic in a sense that each commit leaves the code in a valid state.
For that make git rebase -i
to the point just before the first commit that needs checking, and then replace all pick
with edit
. Then the git applier will stop at each commit one by one giving you a chance to make changes in them all, where needed.