You are viewing gitster

November 17th, 2009


Previous Entry Share Next Entry
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.




Tags:

(3 comments | Leave a comment)

Comments:


From:https://www.google.com/accounts/o8/id?id=AItOawn1jjC5zgHbftU90gcafM3GCJjgSDhneA0
Date:November 18th, 2009 10:46 am (UTC)

Nice writeup

(Link)
Nice writeup, although I'm curious what you refer to with "one interesting aspect of the command". Which of the 'Things to notice' bullets was the interesting aspect? :)
[User Picture]
From:gitster
Date:November 19th, 2009 03:44 am (UTC)

Re: Nice writeup

(Link)
Which one do you think it is ;-)?
[User Picture]
From:David Ongaro
Date:March 22nd, 2011 11:07 pm (UTC)

Great article but...

(Link)
Great article. For me the second bullet point is most interesting.

The only thing is: if I try to apply your examples literally I get merge errors. In the first one I get

error: patch failed: hello.c:1
error: hello.c: patch does not apply

In noticing that the indentation is one space too less in the patch and correcting this I get:

fatal: corrupt patch at line 21

Similar things happen in the following patches.

Of course it's also a great lesson to do the changes manually. But I thought it might be worth to point this out, because surely for you it's easy to fix and it might help other readers.
gitster's journal - Fun with rerere

> Recent Entries
> Archive
> Friends
> Profile

Links
Pages at the k.org
Gifts
貢ぎ物
The latest blog by Gitster

> Go to Top
LiveJournal.com