Subject: [PATCH v2 0/7] rebase --keep-base: imply --reapply-cherry-picks and --no-fork-point

From: "Phillip Wood via GitGitGadget" <[email-masked]>
Date: Wed, 07 Sep 2022 14:37:43 +0000

A while a go Philippe reported [1] that he was surprised 'git rebase
--keep-base' removed commits that had been cherry-picked upstream even
though to branch was not being rebased. I think it is also surprising if
'--keep-base' changes the base of the branch without '--fork-point' being
explicitly given on the command line. This series therefore changes the
default behavior of '--keep-base' to imply '--reapply-cherry-picks' and
'--no-fork-point' so that the base of the branch is unchanged and no commits
are removed.

Thanks to everyone who commented for their reviews, the changes since V1
are:

 * Patch 1: new patch to tighten a couple of existing tests
 * Patch 2: reworded commit message in response to Junio's comments
 * Patch 3: fixed a typo in the commit message spotted by Elijah and tidied
   code formatting
 * Patch 4: new patch to rename a variable suggested by Junio
 * Patch 5: clarified commit message and removed some redundant code spotted
   by Junio
 * Patch 6: improved --reapply-cherry-picks documentation to mention
   --keep-base and vice-versa suggested by Philippe
 * Patch 7: expanded the commit message and documentation in response to
   Junio's comments

[1]
https://lore.kernel.org/git/[email-masked]/

Phillip Wood (7):
  t3416: tighten two tests
  t3416: set $EDITOR in subshell
  rebase: store orig_head as a commit
  rebase: rename merge_base to branch_base
  rebase: factor out branch_base calculation
  rebase --keep-base: imply --reapply-cherry-picks
  rebase --keep-base: imply --no-fork-point

 Documentation/git-rebase.txt     |  32 ++++----
 builtin/rebase.c                 | 129 ++++++++++++++++++-------------
 t/t3416-rebase-onto-threedots.sh |  62 +++++++++++----
 t/t3431-rebase-fork-point.sh     |   2 +-
 4 files changed, 142 insertions(+), 83 deletions(-)

base-commit: afa70145a25e81faa685dc0b465e52b45d2444bd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1323%2Fphillipwood%2Fwip%2Frebase--keep-base-tweaks-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1323/phillipwood/wip/rebase--keep-base-tweaks-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1323

Range-diff vs v1:

 -:  ----------- > 1:  12fb0ac6d5d t3416: tighten two tests
 1:  c1d91a2b190 ! 2:  d6f2f716c77 t3416: set $EDITOR in subshell
     @@ Metadata
       ## Commit message ##
          t3416: set $EDITOR in subshell
      
     -    As $EDITOR is exported setting it in one test affects all subsequent
     -    tests. Avoid this by always setting it in a subshell and remove a
     -    couple of unnecessary call to set_fake_editor.
     +    As $EDITOR is exported, setting it in one test affects all subsequent
     +    tests. Avoid this by always setting it in a subshell. Also remove a
     +    couple of unnecessary call to set_fake_editor where the editor does
     +    not change the todo list.
      
          Signed-off-by: Phillip Wood <[email-masked]>
      
     @@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase -i --onto main...'
       	git rev-parse HEAD^1 >actual &&
       	git rev-parse C^0 >expect &&
       	test_cmp expect actual
     -@@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase -i --onto main...side' '
     +@@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase --onto main...side requires a single merge-base' '
       	git checkout side &&
       	git reset --hard K &&
       
      -	set_fake_editor &&
     - 	test_must_fail git rebase -i --onto main...side J
     + 	test_must_fail git rebase -i --onto main...side J 2>err &&
     + 	grep "need exactly one merge base" err
       '
     - 
      @@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase -i --keep-base main from topic' '
       	git checkout topic &&
       	git reset --hard G &&
     @@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase -i --keep-base mai
       	git rev-parse C >base.expect &&
       	git merge-base main HEAD >base.actual &&
       	test_cmp base.expect base.actual &&
     -@@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase -i --keep-base main from side' '
     +@@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase --keep-base requires a single merge base' '
       	git checkout side &&
       	git reset --hard K &&
       
      -	set_fake_editor &&
     - 	test_must_fail git rebase -i --keep-base main
     + 	test_must_fail git rebase -i --keep-base main 2>err &&
     + 	grep "need exactly one merge base with branch" err
       '
       
      +# This must be the last test in this file
 2:  cced4a48360 ! 3:  9daee95d434 rebase: store orig_head as a commit
     @@ Commit message
          rebase: store orig_head as a commit
      
          Using a struct commit rather than a struct oid to hold orig_head means
     -    that we error out straight away if branch being rebased does not point
     -    to a commit. It also simplifies the code than handles finding the
     -    merge base and fork point as it not longer has to convert from an oid
     -    to a commit.
     +    that we error out straight away if the branch being rebased does not
     +    point to a commit. It also simplifies the code that handles finding
     +    the merge base and fork point as it no longer has to convert from an
     +    oid to a commit.
      
          Signed-off-by: Phillip Wood <[email-masked]>
      
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
      -			get_fork_point(options.upstream_name, head);
      -	}
      +			get_fork_point(options.upstream_name, options.orig_head);
     -+
       
       	if (repo_read_index(the_repository) < 0)
       		die(_("could not read index"));
 -:  ----------- > 4:  cca933a5f1d rebase: rename merge_base to branch_base
 3:  019158db9d2 ! 5:  fc45b996d34 rebase: factor out merge_base calculation
     @@ Metadata
      Author: Phillip Wood <[email-masked]>
      
       ## Commit message ##
     -    rebase: factor out merge_base calculation
     +    rebase: factor out branch_base calculation
      
     -    Separate out calculating the merge base between onto and head from the
     -    check for whether we can fast-forward or not. This means we can skip
     -    the fast-forward checks when the rebase is forced and avoid
     -    calculating the merge-base twice when --keep-base is given.
     +    Separate out calculating the merge base between 'onto' and 'HEAD' from
     +    the check for whether we can fast-forward or not. This means we can skip
     +    the fast-forward checks when the rebase is forced and avoid calculating
     +    the merge-base between 'HEAD' and 'onto' when --keep-base is given.
      
          Signed-off-by: Phillip Wood <[email-masked]>
     -    ---
     -    Note the unnecessary braces around "if (keep_base)" are added here
     -    reduce the churn on the next commit.
      
       ## builtin/rebase.c ##
      @@ builtin/rebase.c: static int can_fast_forward(struct commit *onto, struct commit *upstream,
     @@ builtin/rebase.c: static int can_fast_forward(struct commit *onto, struct commit
       
      -	merge_bases = get_merge_bases(onto, head);
      -	if (!merge_bases || merge_bases->next) {
     --		oidcpy(merge_base, null_oid());
     -+	if (is_null_oid(merge_base))
     +-		oidcpy(branch_base, null_oid());
     ++	if (is_null_oid(branch_base))
       		goto done;
      -	}
       
     --	oidcpy(merge_base, &merge_bases->item->object.oid);
     - 	if (!oideq(merge_base, &onto->object.oid))
     +-	oidcpy(branch_base, &merge_bases->item->object.oid);
     + 	if (!oideq(branch_base, &onto->object.oid))
       		goto done;
       
     +@@ builtin/rebase.c: static int can_fast_forward(struct commit *onto, struct commit *upstream,
     + 	if (!upstream)
     + 		goto done;
     + 
     +-	free_commit_list(merge_bases);
     + 	merge_bases = get_merge_bases(upstream, head);
     + 	if (!merge_bases || merge_bases->next)
     + 		goto done;
      @@ builtin/rebase.c: done:
       	return res && is_linear_history(onto, head);
       }
       
     -+static void fill_merge_base(struct rebase_options *options,
     -+			    struct object_id *merge_base)
     ++static void fill_branch_base(struct rebase_options *options,
     ++			    struct object_id *branch_base)
      +{
      +	struct commit_list *merge_bases = NULL;
      +
      +	merge_bases = get_merge_bases(options->onto, options->orig_head);
      +	if (!merge_bases || merge_bases->next)
     -+		oidcpy(merge_base, null_oid());
     ++		oidcpy(branch_base, null_oid());
      +	else
     -+		oidcpy(merge_base, &merge_bases->item->object.oid);
     ++		oidcpy(branch_base, &merge_bases->item->object.oid);
      +
      +	free_commit_list(merge_bases);
      +}
     @@ builtin/rebase.c: done:
       {
       	struct rebase_options *opts = opt->value;
      @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
     + 		if (!options.onto)
       			die(_("Does not point to a valid commit '%s'"),
       				options.onto_name);
     ++		fill_branch_base(&options, &branch_base);
       	}
      -
     -+	if (keep_base) {
     -+		oidcpy(&merge_base, &options.onto->object.oid);
     -+	} else {
     -+		fill_merge_base(&options, &merge_base);
     -+	}
       	if (options.fork_point > 0)
       		options.restrict_revision =
       			get_fork_point(options.upstream_name, options.orig_head);
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
       	 * in which case we could fast-forward without replacing the commits
       	 * with new commits recreated by replaying their changes.
      -	 *
     --	 * Note that can_fast_forward() initializes merge_base, so we have to
     +-	 * Note that can_fast_forward() initializes branch_base, so we have to
      -	 * call it before checking allow_preemptive_ff.
       	 */
      -	if (can_fast_forward(options.onto, options.upstream, options.restrict_revision,
     --		    options.orig_head, &merge_base) &&
     +-		    options.orig_head, &branch_base) &&
      -	    allow_preemptive_ff) {
      +	if (allow_preemptive_ff &&
      +	    can_fast_forward(options.onto, options.upstream, options.restrict_revision,
     -+			     options.orig_head, &merge_base)) {
     ++			     options.orig_head, &branch_base)) {
       		int flag;
       
       		if (!(options.flags & REBASE_FORCE)) {
 4:  9cd4c372ee4 ! 6:  faad7eaf0d6 rebase --keep-base: imply --reapply-cherry-picks
     @@ Documentation/git-rebase.txt: leave out at most one of A and B, in which case it
       +
       This option is useful in the case where one is developing a feature on
       top of an upstream branch. While the feature is being worked on, the
     + upstream branch may advance and it may not be the best idea to keep
     +-rebasing on top of the upstream but to keep the base commit as-is.
     ++rebasing on top of the upstream but to keep the base commit as-is. As
     ++the base commit is unchanged this option implies `--reapply-cherry-picks`
     ++to avoid losing commits.
     + +
     + Although both this option and `--fork-point` find the merge base between
     + `<upstream>` and `<branch>`, this option uses the merge base as the _starting
     +@@ Documentation/git-rebase.txt: See also INCOMPATIBLE OPTIONS below.
     + Note that commits which start empty are kept (unless `--no-keep-empty`
     + is specified), and commits which are clean cherry-picks (as determined
     + by `git log --cherry-mark ...`) are detected and dropped as a
     +-preliminary step (unless `--reapply-cherry-picks` is passed).
     ++preliminary step (unless `--reapply-cherry-picks` or `--keep-base` is
     ++passed).
     + +
     + See also INCOMPATIBLE OPTIONS below.
     + 
     +@@ Documentation/git-rebase.txt: See also INCOMPATIBLE OPTIONS below.
     + 	upstream changes, the behavior towards them is controlled by
     + 	the `--empty` flag.)
     + +
     +-By default (or if `--no-reapply-cherry-picks` is given), these commits
     +-will be automatically dropped.  Because this necessitates reading all
     +-upstream commits, this can be expensive in repos with a large number
     +-of upstream commits that need to be read.  When using the 'merge'
     +-backend, warnings will be issued for each dropped commit (unless
     +-`--quiet` is given). Advice will also be issued unless
     +-`advice.skippedCherryPicks` is set to false (see linkgit:git-config[1]).
     ++
     ++In the absence of `--keep-base` (or if `--no-reapply-cherry-picks` is
     ++given), these commits will be automatically dropped.  Because this
     ++necessitates reading all upstream commits, this can be expensive in
     ++repositories with a large number of upstream commits that need to be
     ++read. When using the 'merge' backend, warnings will be issued for each
     ++dropped commit (unless `--quiet` is given). Advice will also be issued
     ++unless `advice.skippedCherryPicks` is set to false (see
     ++linkgit:git-config[1]).
     ++
     + +
     + `--reapply-cherry-picks` allows rebase to forgo reading all upstream
     + commits, potentially improving performance.
      
       ## builtin/rebase.c ##
      @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
       			die(_("options '%s' and '%s' cannot be used together"), "--keep-base", "--root");
       	}
      +	/*
     -+	 * --keep-base defaults to --reapply-cherry-picks as it is confusing if
     -+	 * commits disappear when using this option.
     ++	 * --keep-base defaults to --reapply-cherry-picks to avoid losing
     ++	 * commits when using this option.
      +	 */
      +	if (options.reapply_cherry_picks < 0)
      +		options.reapply_cherry_picks = keep_base;
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
       
       	if (gpg_sign)
      @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
     + 				options.onto_name);
     + 		fill_branch_base(&options, &branch_base);
       	}
     - 	if (keep_base) {
     - 		oidcpy(&merge_base, &options.onto->object.oid);
     -+		if (options.reapply_cherry_picks)
     -+			options.upstream = options.onto;
     - 	} else {
     - 		fill_merge_base(&options, &merge_base);
     - 	}
     ++	if (keep_base && options.reapply_cherry_picks)
     ++		options.upstream = options.onto;
     ++
     + 	if (options.fork_point > 0)
     + 		options.restrict_revision =
     + 			get_fork_point(options.upstream_name, options.orig_head);
      
       ## t/t3416-rebase-onto-threedots.sh ##
     -@@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase -i --keep-base main from side' '
     - 	test_must_fail git rebase -i --keep-base main
     +@@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase --keep-base requires a single merge base' '
     + 	grep "need exactly one merge base with branch" err
       '
       
      +test_expect_success 'rebase --keep-base keeps cherry picks' '
 5:  68bcd10949e ! 7:  6410b101d7f rebase --keep-base: imply --no-fork-point
     @@ Commit message
          changes the base of the branch without --fork-point being explicitly
          given on the command line.
      
     +    The combination of --keep-base with an explicit --fork-point is still
     +    supported even though --fork-point means we do not keep the same base
     +    if the upstream branch has been rewound.  We do this in case anyone is
     +    relying on this behavior which is tested in t3431[1]
     +
     +    [1] https://lore.kernel.org/git/20200715032014.GA10818@generichostname/
     +
          Signed-off-by: Phillip Wood <[email-masked]>
      
       ## Documentation/git-rebase.txt ##
     @@ Documentation/git-rebase.txt: leave out at most one of A and B, in which case it
       +
       This option is useful in the case where one is developing a feature on
       top of an upstream branch. While the feature is being worked on, the
     +@@ Documentation/git-rebase.txt: When `--fork-point` is active, 'fork_point' will be used instead of
     + <branch>` command (see linkgit:git-merge-base[1]).  If 'fork_point'
     + ends up being empty, the `<upstream>` will be used as a fallback.
     + +
     +-If `<upstream>` is given on the command line, then the default is
     +-`--no-fork-point`, otherwise the default is `--fork-point`. See also
     +-`rebase.forkpoint` in linkgit:git-config[1].
     ++If `<upstream>` or `--keep-base` is given on the command line, then
     ++the default is `--no-fork-point`, otherwise the default is
     ++`--fork-point`. See also `rebase.forkpoint` in linkgit:git-config[1].
     + +
     + If your branch was based on `<upstream>` but `<upstream>` was rewound and
     + your branch contains commits which were dropped, this option can be used
      
       ## builtin/rebase.c ##
      @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
       		if (options.root)
       			die(_("options '%s' and '%s' cannot be used together"), "--keep-base", "--root");
      +		/*
     -+		 * --keep-base ignores config.forkPoint as it is confusing if
     -+		 * the branch base changes when using this option.
     ++		 * --keep-base defaults to --no-fork-point to keep the
     ++		 * base the same.
      +		 */
      +		if (options.fork_point < 0)
      +			options.fork_point = 0;
       	}
       	/*
     - 	 * --keep-base defaults to --reapply-cherry-picks as it is confusing if
     + 	 * --keep-base defaults to --reapply-cherry-picks to avoid losing
      
       ## t/t3431-rebase-fork-point.sh ##
      @@ t/t3431-rebase-fork-point.sh: test_rebase () {

-- 
gitgitgadget