1.7 cherry-pick, revert, rebase
Book can be downloaded from here
You have already seen what merge conflicts (the ones that come about when we
are merging branches) look like. There are 3 other operations that can produce
conflicts:
Even though the basics are the same, each one of the sections of the CB has a different
meaning
from what they were during a merge.
1.7.1 cherry-pick - Example 12
In git 2.27, this change was introduced:
Listing 1.115:example 12 - change from 3be7efcafceeae34$ git show --pretty= 3be7efcafceeae34 diff --git a/commit-graph.c b/commit-graph.c index f013a84e29..e4f1a5b2f1 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -23,6 +23,7 @@ #define GRAPH_CHUNKID_DATA 0x43444154 /* "CDAT" */ #define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* "EDGE" */ #define GRAPH_CHUNKID_BASE 0x42415345 /* "BASE" */ +#define MAX_NUM_CHUNKS 5 #define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16) @@ -1350,8 +1351,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx) int fd; struct hashfile *f; struct lock_file lk = LOCK_INIT; - uint32_t chunk_ids[6]; - uint64_t chunk_offsets[6]; + uint32_t chunk_ids[MAX_NUM_CHUNKS + 1]; + uint64_t chunk_offsets[MAX_NUM_CHUNKS + 1]; const unsigned hashsz = the_hash_algo->rawsz; struct strbuf progress_title = STRBUF_INIT; int num_chunks = 3;
We can see the way that chunk_ids and chunk_offsets’s sizes are changed to be
defined by a macro.
As an experiment, we want to backport this change on top of v2.22.4:
Listing 1.116:example 12 - backporting 3be7efcafceeae34$ git checkout v2.22.4 HEAD is now at c9808fa014 Git 2.22.4 $ git cherry-pick 3be7efcafceeae34 Auto-merging commit-graph.c CONFLICT (content): Merge conflict in commit-graph.c error: could not apply 3be7efcafc... commit-graph: define and use MAX_NUM_CHUNKS hint: after resolving the conflicts, mark the corrected paths hint: with ’git add <paths>’ or ’git rm <paths>’ hint: and commit the result with ’git commit’ $ git status HEAD detached at v2.22.4 You are currently cherry-picking commit 3be7efcafc. (fix conflicts and run "git cherry-pick --continue") (use "git cherry-pick --skip" to skip this patch) (use "git cherry-pick --abort" to cancel the cherry-pick operation) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: commit-graph.c no changes added to commit (use "git add" and/or "git commit -a")
What does the conflict look like? There are two CBs in commit-graph.c. The
first CB looks like this:
CB#1
Listing 1.117:example 12 - CB#1 in commit-graph.c25<<<<<<< HEAD 26||||||| parent of 3be7efcafc... commit-graph: define and use MAX_NUM_CHUNKS 27#define GRAPH_CHUNKID_BASE 0x42415345 /* "BASE" */ 28======= 29#define GRAPH_CHUNKID_BASE 0x42415345 /* "BASE" */ 30#define MAX_NUM_CHUNKS 5 31>>>>>>> 3be7efcafc... commit-graph: define and use MAX_NUM_CHUNKS
It looks a lot like the conflicts we have seen so far, right? Just like in a merge,
there are 3 sections in the CB and I will keep on using the same terms/achronyms
we have used so far. The first one is UB, in other words, code as it is on HEAD, in
our case, on tag v2.22.4, as that is where we are currently working. Unlike
normal merges where we get to see the common ancestor’s code in MB
and the other branch’s code in LB, during a cherry-pick operation the
other two blocks display something different, as described on the conflict
markers. MB shows us the content of the parent of the revision we are trying to
cherry-pick .
LB shows us code as it is on the revision 3be7efcafc, what we are trying to
cherry-pick. Even though the MB and LB relate to different concepts from a merge,
the way the analysis has to be carried out is the same.
dMU#1
The macro GRAPH_CHUNKID_BASE (present in line 27) was removed.
dML#1
The macro MAX_NUM_CHUNKS was added (line 30).
Resolution#1
I don’t think there’s much difference working from UB or LB.... it’s just a tiny bit
simpler to delete things than copying from another place so I will start working from
LB:
Listing 1.118:example 12 - LB#1 in commit-graph.c28======= 29#define GRAPH_CHUNKID_BASE 0x42415345 /* "BASE" */ 30#define MAX_NUM_CHUNKS 5 31>>>>>>> 3be7efcafc... commit-graph: define and use MAX_NUM_CHUNKS
Then, as dMU is requesting, we remove GRAPH_CHUNKID_BASE.
Listing 1.119:example 12 - LB#1 in commit-graph.c28======= 29#define MAX_NUM_CHUNKS 5 30>>>>>>> 3be7efcafc... commit-graph: define and use MAX_NUM_CHUNKS
Remove the other sections of the conflict and the markers and we are
done.
Listing 1.120:example 12 - Solution of CB#1 in commit-graph.c24#define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* "EDGE" */ 25#define MAX_NUM_CHUNKS 5 26 27#define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16)
And then we go into CB#2 in commit-graph.c:
CB#2
Listing 1.121:example 12 - CB#2 in commit-graph.c1031<<<<<<< HEAD 1032 uint32_t chunk_ids[5]; 1033 uint64_t chunk_offsets[5]; 1034||||||| parent of 3be7efcafc... commit-graph: define and use MAX_NUM_CHUNKS 1035 uint32_t chunk_ids[6]; 1036 uint64_t chunk_offsets[6]; 1037======= 1038 uint32_t chunk_ids[MAX_NUM_CHUNKS + 1]; 1039 uint64_t chunk_offsets[MAX_NUM_CHUNKS + 1]; 1040>>>>>>> 3be7efcafc... commit-graph: define and use MAX_NUM_CHUNKS
dMU#2
The size of chunk_ids and chunk_offsets was changed from 6 to 5.
dML#2
Got rid of the numeric value and replaced it for MAX_NUM_CHUNKS + 1,
using the macro we defined on the previous CB.
Resolution#2
In this special case we can keep code from LB as is because the change of 6 for 5
has no incidence on the requirement: We still need to use the macro. So:
Listing 1.122:example 12 - Solution of CB#2 in commit-graph.c1030 struct lock_file lk = LOCK_INIT; 1031 uint32_t chunk_ids[MAX_NUM_CHUNKS + 1]; 1032 uint64_t chunk_offsets[MAX_NUM_CHUNKS + 1]; 1033 const unsigned hashsz = the_hash_algo->rawsz;
And we are done! ... Or so you think! Do you remember the intent of the revision
we are trying to cherry-pick? It was related to changing the way we define the size of
the arrays to use a macro. This is probably so that if we needed to change the size of
the arrays from 6 to 20 in the future, we would only adjust the value of
MAX_NUM_CHUNKS from 5 to 19 and it would hit both arrays at
the same time. Nice! But we are not applying it on a revision where the
size of the arrays is 6. We have arrays of size 5 and it has to remain the
same.
Which means that we need to set up the macro to 4, so that the arrays end up with
the correct original value when using the macro. Then we need to go back to the
previous CB and we adjust the macro value:
Listing 1.123:example 12 - Adjustment of Solution of CB#1 in commit-graph.c24#define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* "EDGE" */ 25#define MAX_NUM_CHUNKS 4 26 27#define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16)
And now we are done!
Question: Would it be OK to leave the macro with 5 and change the arrays in
CB#2 to use MAX_NUMS_CHUNKS instead? Like this:
Listing 1.124:example 12 - Alternative solution to CB#2 in commit-graph.c1030 struct lock_file lk = LOCK_INIT; 1031 uint32_t chunk_ids[MAX_NUM_CHUNKS]; 1032 uint64_t chunk_offsets[MAX_NUM_CHUNKS]; 1033 const unsigned hashsz = the_hash_algo->rawsz;
From the plain coding point if view, it might be correct
.
Code compiles and behaves the same, right? But you are breaking the intent of the
revision you are cherry-picking. Just so that you can glance the consequences of this
decision, what would happen if, later on, you needed to cherry-pick another revision
that uses this same macro? Now that you have broken the values that you are
using for it, you will have to do more adjustments on the code than the
original intent of the other revisions you were cherry-picking then.... and you
might not even get conflicts when you cherry-pick them. Will you be able to
keep this change in mind so that you are able to catch those situations later
on and correct the code? I know I would.... for the first 30 seconds after I
modified the code, but I don’t think I would be able to keep it for much
longer. And I’m sure it’s extremely unlikey anyone else will keep this in
mind. All in all: You better stick to the original intent. You have been
forewarned.
1.7.2 revert - Example 13
When you want to take back the changes introduced by a revision, the operation you have to apply
is revert .
Let’s consider a little case from git.
From git repo, checkout tag v2.26.2. We will revert revision 22a69fda197f.
Let me show you what the revision that I want to revert did originally to
builtin/rebase.c:
Listing 1.125:example 13 - Original revision$ git show --pretty="" 22a69fda197f -- builtin/rebase.c diff --git a/builtin/rebase.c b/builtin/rebase.c index 8081741f8a..faa4e0d406 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -453,8 +453,9 @@ int cmd_rebase__interactive(int argc, const char **argv, const char *prefix) OPT_NEGBIT(0, "ff", &opts.flags, N_("allow fast-forward"), REBASE_FORCE), OPT_BOOL(0, "keep-empty", &opts.keep_empty, N_("keep empty commits")), - OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message, - N_("allow commits with empty messages")), + OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message, + N_("allow commits with empty messages"), + PARSE_OPT_HIDDEN), OPT_BOOL(0, "rebase-merges", &opts.rebase_merges, N_("rebase merge commits")), OPT_BOOL(0, "rebase-cousins", &opts.rebase_cousins, N_("keep original branch points of cousins")), @@ -1495,9 +1496,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) OPT_STRING_LIST(’x’, "exec", &exec, N_("exec"), N_("add exec lines after each commit of the " "editable list")), - OPT_BOOL(0, "allow-empty-message", - &options.allow_empty_message, - N_("allow rebasing commits with empty messages")), + OPT_BOOL_F(0, "allow-empty-message", + &options.allow_empty_message, + N_("allow rebasing commits with empty messages"), + PARSE_OPT_HIDDEN), {OPTION_STRING, ’r’, "rebase-merges", &rebase_merges, N_("mode"), N_("try to rebase merges instead of skipping them"),
The revision we want to revert changed the original calls to OPT_BOOL() for
defining allow-empty-message to OPT_BOOL_F() and adjusted the parameters.
That means that if we want to revert this revision, we need to end up with the
original OPT_BOOL() call to define allow-empty-message in place after the
revert is finished, right? Let’s try:
Listing 1.126:example 13 - Reverting$ git revert --no-commit 22a69fda197f Auto-merging builtin/rebase.c CONFLICT (content): Merge conflict in builtin/rebase.c Auto-merging Documentation/git-rebase.txt error: could not revert 22a69fda19... git-rebase.txt: update description of --allow-empty-message hint: after resolving the conflicts, mark the corrected paths hint: with ’git add <paths>’ or ’git rm <paths>’ $ git status HEAD detached at v2.26.2 You are currently reverting commit 22a69fda19. (fix conflicts and run "git revert --continue") (use "git revert --skip" to skip this patch) (use "git revert --abort" to cancel the revert operation) Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: Documentation/git-rebase.txt Unmerged paths: (use "git restore --staged <file>..." to unstage) (use "git add <file>..." to mark resolution) both modified: builtin/rebase.c
There is a CB in builtin/rebase.c:
Listing 1.127:example 13 - CB in builtin/rebase.c473<<<<<<< HEAD 474 { OPTION_CALLBACK, ’k’, "keep-empty", &options, NULL, 475 N_("(DEPRECATED) keep empty commits"), 476 PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, 477 parse_opt_keep_empty }, 478 OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message, 479 N_("allow commits with empty messages"), 480 PARSE_OPT_HIDDEN), 481||||||| 22a69fda19... git-rebase.txt: update description of --allow-empty-message 482 OPT_BOOL(0, "keep-empty", &opts.keep_empty, N_("keep empty commits")), 483 OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message, 484 N_("allow commits with empty messages"), 485 PARSE_OPT_HIDDEN), 486======= 487 OPT_BOOL(0, "keep-empty", &opts.keep_empty, N_("keep empty commits")), 488 OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message, 489 N_("allow commits with empty messages")), 490>>>>>>> parent of 22a69fda19... git-rebase.txt: update description of --allow-empty-message
This is not too different from what we saw in cherry-pick, right? UB is the way
the code is on v2.26.2, as that is the revision we started working from. The
interesting part is in the other two blocks. You can see from the markers that MB is
the way code looked on the revision we are trying to revert (in other words, the code
the way it looked after the revision was applied) and LB is the way code
looks on its parent revision (in other words, the way the code looked before)
.
This means that we, just like in the merges and cherry-picks we have already solved,
need to go through dMU and dML without any changes in methodology so it’s
business as usual.
dMU
The way keep-empty is defined is changed from a call to OPT_BOOL() in line
482 to a hand-made definition in lines 474-477.
dML
The way allow-empty-message is defined is changed from a call to
OPT_BOOL_F() in lines 483-485 to OPT_BOOL() in lines 488-489,
which also removed PARSE_OPT_HIDDEN as a parameter to the new
call.
Resolution
I will stay working on UB, just because the way keep-empty was changed from the
MB is bigger than the change that was done for allow-empty-message in dML.
Then I will change the macro call from OPT_BOOL_F() to OPT_BOOL() (lines
478-480):
Listing 1.128:example 13 - Step 1 UB in builtin/rebase.c473<<<<<<< HEAD 474 { OPTION_CALLBACK, ’k’, "keep-empty", &options, NULL, 475 N_("(DEPRECATED) keep empty commits"), 476 PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, 477 parse_opt_keep_empty }, 478 OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message, 479 N_("allow commits with empty messages"), 480 PARSE_OPT_HIDDEN), 481||||||| 22a69fda19... git-rebase.txt: update description of --allow-empty-message
Remove PARSE_OPT_HIDDEN as a parameter (present on line 480):
Listing 1.129:example 13 - Step 2 UB in builtin/rebase.c473<<<<<<< HEAD 474 { OPTION_CALLBACK, ’k’, "keep-empty", &options, NULL, 475 N_("(DEPRECATED) keep empty commits"), 476 PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, 477 parse_opt_keep_empty }, 478 OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message, 479 N_("allow commits with empty messages")), 480||||||| 22a69fda19... git-rebase.txt: update description of --allow-empty-message
Reposition the second line for the argument on the second line of the macro call
to align with the position of the call (line 479):
Listing 1.130:example 13 - Step 3 UB in builtin/rebase.c473<<<<<<< HEAD 474 { OPTION_CALLBACK, ’k’, "keep-empty", &options, NULL, 475 N_("(DEPRECATED) keep empty commits"), 476 PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, 477 parse_opt_keep_empty }, 478 OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message, 479 N_("allow commits with empty messages")), 480||||||| 22a69fda19... git-rebase.txt: update description of --allow-empty-message
And now we are done with the reversion:
Listing 1.131:example 13 - Solution of CB in builtin/rebase.c470 struct option options[] = { 471 OPT_NEGBIT(0, "ff", &opts.flags, N_("allow fast-forward"), 472 REBASE_FORCE), 473 { OPTION_CALLBACK, ’k’, "keep-empty", &options, NULL, 474 N_("(DEPRECATED) keep empty commits"), 475 PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, 476 parse_opt_keep_empty }, 477 OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message, 478 N_("allow commits with empty messages"), 479 OPT_BOOL(0, "rebase-merges", &opts.rebase_merges, N_("rebase merge commits")),
And we can see how we ended up with a call to OPT_BOOL() to define
allow-empty-message, just what we wanted to get with the reversion.
Just so that you can see that the same rules apply when analyzing a reversion, try
solving the conflict working from LB and bring over the changes introduced in
dMU. You should end up with the same code.
1.7.3 rebase - Example 14
Let’s play a little bit. From git repo, checkout revision ce9baf234f. Let’s take a look
at a section of builtin/push.c on this revision:
Listing 1.132:example 14 - Section of builtin/push.c in ce9baf234f548 OPT_BIT(’f’, "force", &flags, N_("force updates"), TRANSPORT_PUSH_FORCE), 549 { OPTION_CALLBACK, 550 0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), 551 N_("require old value of ref to be at this value"), 552 PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option }, 553 OPT_CALLBACK(0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", 554 N_("control recursive pushing of submodules"), option_parse_recurse_submodules), 555 OPT_BOOL_F( 0 , "thin", &thin, N_("use thin pack"), PARSE_OPT_NOCOMPLETE),
Take a close look at the way <refname>:<expect> and recurse-submodules
are defined. <refname>:<expect> is defined using a struct by hand in lines
549-552 and recurse-submodules is defined with a call to OPT_CALLBACK()
in lines 553-554.
Let’s ask git to rebase on top of
6652716200 .
You should see a CB related to the section of builtin/push.c that I showed you
before:
Listing 1.133:example 14 - CB in builtin/push.c550 OPT_BIT(’f’, "force", &flags, N_("force updates"), TRANSPORT_PUSH_FORCE), 551<<<<<<< HEAD 552 OPT_CALLBACK_F(0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), 553 N_("require old value of ref to be at this value"), 554 PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option), 555 { OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", 556 N_("control recursive pushing of submodules"), 557 PARSE_OPT_OPTARG, option_parse_recurse_submodules }, 558||||||| parent of ce9baf234f... push: unset PARSE_OPT_OPTARG for --recurse-submodules 559 { OPTION_CALLBACK, 560 0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), 561 N_("require old value of ref to be at this value"), 562 PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option }, 563 { OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", 564 N_("control recursive pushing of submodules"), 565 PARSE_OPT_OPTARG, option_parse_recurse_submodules }, 566======= 567 { OPTION_CALLBACK, 568 0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), 569 N_("require old value of ref to be at this value"), 570 PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option }, 571 OPT_CALLBACK(0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", 572 N_("control recursive pushing of submodules"), option_parse_recurse_submodules), 573>>>>>>> ce9baf234f... push: unset PARSE_OPT_OPTARG for --recurse-submodules 574 OPT_BOOL_F( 0 , "thin", &thin, N_("use thin pack"), PARSE_OPT_NOCOMPLETE),
You remember the revision you were on when you ran rebase? It was on
ce9baf234f, right? Look at UB. Does it look the way it was in ce9baf234f? No?
Ok. How about LB? Oh, it’s there! And then, what is on UB? You remember
that I said that UB always contains code as it is in HEAD? Well, that is
correct, even in this case. The trick that you have to realize is that when
you run a rebase git first checks out the base branch for the rebase and
then starts to apply the revisions that you ask to rebase on top of the base
branch.
So, in our current example, in UB you will see the code as it is in 6652716200
and in LB you will see the code from ce9baf234f. In MB you will see the parent of
the revision that is being rebased at the time, so in this case it will hold the parent of
ce9baf234f. You can see the details about the revision used in MB and LB in the
conflict markers, just in case.
Now that we have cleared that up, we continue with the analysis as we have done
so far, no changes:
dMU
The way that <refname>:<expect> is being defined is from by-hand in lines
559-562 to a call to OPT_CALLBACK_F() in lines 552-554.
dML
The way recurse-submodules is being defined is changed from by hand in lines
563-565 to a call to OPT_CALLBACK() in lines 571-572.
Resolution
I don’t see much difference between working from UB and LB. Let’s start working
from UB:
Listing 1.134:example 14 - Step 1 UB in builtin/push.c550<<<<<<< HEAD 551 OPT_CALLBACK_F(0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), 552 N_("require old value of ref to be at this value"), 553 PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option), 554 { OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", 555 N_("control recursive pushing of submodules"), 556 PARSE_OPT_OPTARG, option_parse_recurse_submodules }, 557||||||| parent of ce9baf234f... push: unset PARSE_OPT_OPTARG for --recurse-submodules
Then we modify the way that recurse-submodules is defined as in dML:
Listing 1.135:example 14 - Step 2 UB in builtin/push.c550<<<<<<< HEAD 551 OPT_CALLBACK_F(0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), 552 N_("require old value of ref to be at this value"), 553 PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option), 554 OPT_CALLBACK(0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", 555 N_("control recursive pushing of submodules"), option_parse_recurse_submodules), 556||||||| parent of ce9baf234f... push: unset PARSE_OPT_OPTARG for --recurse-submodules
And we are done:
Listing 1.136:example 14 - Solution of CB in builtin/push.c550 OPT_BIT(’f’, "force", &flags, N_("force updates"), TRANSPORT_PUSH_FORCE), 551 OPT_CALLBACK_F(0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), 552 N_("require old value of ref to be at this value"), 553 PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option), 554 OPT_CALLBACK(0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", 555 N_("control recursive pushing of submodules"), option_parse_recurse_submodules), 556 OPT_BOOL_F( 0 , "thin", &thin, N_("use thin pack"), PARSE_OPT_NOCOMPLETE),
We add the file to index, nothing else is pending and so we can continue with the
rebase .
1.7.4 Tips
Regarless of the operation being done (merge, cherry-pick, revert, rebase), always use
the same technique to solve conflicts: dMU/dML, Resolution.
Copyright 2020 Edmundo Carmona Antoranz