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

", message); } /* * Show greetings */ void greetings(char *message) { hello(message); printf("Your message is %d bytes long

", (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

", 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

", (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

", 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

", @@ -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

", 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

", 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.









