06:13 pm - Fun with rerere
When you start using the topic branch workflow, you would merge topics often into a throw-away testing branch, and from time to time, end up performing the same conflict resolution over and over again. Git has a mechanism called rerere to help you in such a situation.
Even people who use rerere often do not realize one interesting aspect of the command, and this article is about showing off that part.
Let's pretend that you start from this base version.
$ mkdir /var/tmp/practice-rerere
$ cd /var/tmp/practice-rerere
$ git init
$ cat >hello.c <<\EOF
#include <stdio.h>
#include <string.h>
/*
* Say the message
*/
void hello(char *message)
{
printf("%s\n", message);
}
/*
* Show greetings
*/
void greetings(char *message)
{
hello(message);
printf("Your message is %d bytes long\n",
(int) strlen(message));
}
/*
* Main program
*/
int main(int ac, char **av)
{
greetings(av[1]);
return 0;
}
EOF
A very simple "hello world" program. You can try it like this
if you want to.
$ cc -o hello ./hello.c && ./hello "hello world"
hello world
Your message is 11 bytes long
Commit it as the initial version.
$ git add hello.c
$ git commit -m initial
Now, let's create a topic to work on upcasing the first word
of the message.
$ git checkout -b upcase-first
$ git apply <<\EOF && git commit -a -m "upcase first word"
diff --git a/hello.c b/hello.c
index e0dbe10..7fcb4f3 100644
--- a/hello.c
+++ b/hello.c
@@ -1,11 +1,16 @@
+#include <ctype.h>
#include <stdio.h>
#include <string.h>
/*
* Say the message
*/
-void hello(char *message)
+void hello(char *message, int upcase_first)
{
+ char *cp;
+ for (cp = message; *cp && !isspace(*cp); cp++)
+ *cp = toupper(*cp);
+
printf("%s\n", message);
}
@@ -14,7 +19,7 @@ void hello(char *message)
*/
void greetings(char *message)
{
- hello(message);
+ hello(message, 1);
printf("Your message is %d bytes long\n",
(int) strlen(message));
}
EOF
$ cc -o hello ./hello.c && ./hello "hello world"
HELLO world
Your message is 11 bytes long
While you are working on this change, let's pretend that
somebody committed a change on a 'const-fix' branch to
tighten constness.
$ git checkout -b const-fix master
$ git apply <<\EOF && git commit -a -m "constness fix"
diff --git a/hello.c b/hello.c
index e0dbe10..9970faa 100644
--- a/hello.c
+++ b/hello.c
@@ -4,7 +4,7 @@
/*
* Say the message
*/
-void hello(char *message)
+void hello(const char *message)
{
printf("%s\n", message);
}
@@ -12,7 +12,7 @@ void hello(char *message)
/*
* Show greetings
*/
-void greetings(char *message)
+void greetings(const char *message)
{
hello(message);
printf("Your message is %d bytes long\n",
@@ -22,7 +22,7 @@ void greetings(char *message)
/*
* Main program
*/
-int main(int ac, char **av)
+int main(int ac, const char **av)
{
greetings(av[1]);
return 0;
EOF
Now, you would want to try your topic with this new
branch to make sure they play well together. Don't
merge 'const-fix' into your topic for this, though.
Your 'upcase-first' branch is about upcasing the first
word in the message, and should only contain commits
that are relevant to that goal.
Instead, you would try merging on a throw-away branch.
Recent git allows you to "detach HEAD", so let's make
use of that.
$ git checkout upcase-first^0
Note: moving to 'upcase-first^0' which isn't a local branch
If you want to create a new branch from this checkout, you may do so
(now or later) by using -b with the checkout command again. Example:
git checkout -b <new_branch_name>
HEAD is now at 362a9fb... upcase first
This temporarily checks out the named commit (in this case, the
commit at the tip of your upcase-first branch). Since the purpose
of this article is to show the power of rerere, we will do a little
magic here to enable it.
$ mkdir .git/rr-cache
Don't worry too much about it for now. This needs to be done only
once in your repository. Now to the merge.
$ git merge const-fix
Auto-merging hello.c
CONFLICT (content): Merge conflict in hello.c
Recorded preimage for 'hello.c'
Automatic merge failed; fix conflicts and then commit the result.
This results in a merge conflict. Let's examine what happened.
$ cat hello.c
#include <ctype.h>
#include <stdio.h>
#include <string.h>
/*
* Say the message
*/
<<<<<<< HEAD
void hello(char *message, int upcase_first)
=======
void hello(const char *message)
>>>>>>> const-fix
{
char *cp;
for (cp = message; *cp && !isspace(*cp); cp++)
*cp = toupper(*cp);
printf("%s\n", message);
}
/*
* Show greetings
...
There is a constness change on the 'const-fix' side, while
your side added an "upcase_first" argument to the function
hello(). The conflicted part is shown enclosed in
<<<<<<< HEAD
void hello(char *message, int upcase_first) --- your changes
=======
void hello(const char *message) --- their changes
>>>>>>> const-fix
and you would resolve it to read like this.
void hello(const char *message, int upcase_first)
But that is not enough. The way you implemented your upcase-first
is to use a pointer cp that walks the message and upcase the first
word in place. Now you are not allowed to overwrite message, so
a different solution is necessary.
Your new hello() function may look like this:
void hello(const char *message, int upcase_first)
{
const char *cp;
for (cp = message; *cp && !isspace(*cp); cp++)
putchar(toupper(*cp));
message = cp;
printf("%s\n", message);
}
After editing hello.c like that, test the result.
$ cc -o hello ./hello.c && ./hello "hello world"
HELLO world
Your message is 11 bytes long
You are satisfied that your changes, even though they have conflicts
with somebody else's changes, still work well. You can go back and continue
working on your topic, but before doing so, tell git that you are
done, and the easiest way to do so is to make a throw-away commit.
$ git commit -a -m 'test resolution'
Recorded resolution for 'hello.c'.
[detached HEAD 2fe010b] test resolution
Notice it says "Recorded resolution"? Now let's go back and keep working
on the upcase-first topic.
$ git checkout upcase-first
Previous HEAD position was 2fe010b... test resolution
Switched to branch 'upcase-first'
$ git apply <<\EOF && git commit -a -m "add comment"
diff --git a/hello.c b/hello.c
index 9970faa..53abee2 100644
--- a/hello.c
+++ b/hello.c
@@ -20,7 +20,7 @@ void greetings(const char *message)
}
/*
- * Main program
+ * Main program - give greetings with the first word upcased.
*/
int main(int ac, char **av)
{
EOF
$ cc -o hello ./hello.c && ./hello "hello world"
HELLO world
Your message is 11 bytes long
Now let's try the "test merge with const-fix" again.
$ git checkout HEAD^0
$ git merge const-fix
Auto-merging hello.c
CONFLICT (content): Merge conflict in hello.c
Resolved 'hello.c' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.
This again results in a merge conflict, but notice that the message
says "Resolved 'hello.c' using previous resolution."
If you look at hello.c, you actually do not see any conflict markers.
Instead, the hello() function is already updated with the change you
made earlier, even though you are not merging exactly the versions
as you tried earlier.
If you are curious, here is how to check what conflict you got:
$ git checkout --conflict=merge hello.c
$ cat hello.c
Try it. You will notice that:
- The branches produced the conflict exactly the same way. The hello() function has different sets of arguments.
- The body of the hello() function did not have any conflict; it merged cleanly at the textual level, but it is wrong as the merge result.
Running "git rerere" explicitly at this point will again resolve
the conflict for you.
$ git rerere
Resolved 'hello.c' using previous resolution.
Things to notice:
- Rerere remembers how you chose to resolve the conflicted regions;
- Rerere also remembers how you touched up outside the conflicted regions to adjust to semantic changes;
- Rerere can reuse previous resolution even though you were merging two branches with different contents than the one you resolved earlier.
Even people who have been using rerere for a long time often fail to notice the last point.
|