diff mbox series

rebase: add `--update-refs=interactive`

Message ID 20250210191650.316329-1-intelfx@intelfx.name (mailing list archive)
State New
Headers show
Series rebase: add `--update-refs=interactive` | expand

Commit Message

Ivan Shapovalov Feb. 10, 2025, 7:16 p.m. UTC
In rebase-heavy workflows involving multiple interdependent feature
branches, typing out `--update-refs` quickly becomes tiring, which
can be mitigated with setting the `rebase.updateRefs` git-config option
to perform update-refs by default.

However, the utility of `rebase.updateRefs` is somewhat limited because
you rarely want it in a non-interactive rebase (as it does not give you
the chance to review the update-refs candidates, likely leading to
updating refs that you didn't want updated -- I made quite an amount
of mess by setting this option and subsequently forgetting about it).

Try to find a middle ground by introducing a third value,
`--update-refs=interactive` (and `rebase.updateRefs=interactive`)
which means `--update-refs` when starting an interactive rebase and
`--no-update-refs` otherwise. This option is primarily intended to be
used in the gitconfig, but is also accepted on the command line
for completeness.

Signed-off-by: Ivan Shapovalov <intelfx@intelfx.name>
---
 Documentation/config/rebase.txt |  7 +++-
 Documentation/git-rebase.txt    |  8 +++-
 builtin/rebase.c                | 72 +++++++++++++++++++++++++++++----
 3 files changed, 77 insertions(+), 10 deletions(-)

Comments

Ben Knoble Feb. 10, 2025, 8:22 p.m. UTC | #1
On Mon, Feb 10, 2025 at 2:17 PM Ivan Shapovalov <intelfx@intelfx.name> wrote:
>
> In rebase-heavy workflows involving multiple interdependent feature
> branches, typing out `--update-refs` quickly becomes tiring, which
> can be mitigated with setting the `rebase.updateRefs` git-config option
> to perform update-refs by default.
>
> However, the utility of `rebase.updateRefs` is somewhat limited because
> you rarely want it in a non-interactive rebase (as it does not give you
> the chance to review the update-refs candidates, likely leading to
> updating refs that you didn't want updated -- I made quite an amount
> of mess by setting this option and subsequently forgetting about it).
>
> Try to find a middle ground by introducing a third value,
> `--update-refs=interactive` (and `rebase.updateRefs=interactive`)
> which means `--update-refs` when starting an interactive rebase and
> `--no-update-refs` otherwise. This option is primarily intended to be
> used in the gitconfig, but is also accepted on the command line
> for completeness.
>
> Signed-off-by: Ivan Shapovalov <intelfx@intelfx.name>
> ---
>  Documentation/config/rebase.txt |  7 +++-
>  Documentation/git-rebase.txt    |  8 +++-
>  builtin/rebase.c                | 72 +++++++++++++++++++++++++++++----
>  3 files changed, 77 insertions(+), 10 deletions(-)
>
> diff --git a/Documentation/config/rebase.txt b/Documentation/config/rebase.txt
> index c6187ab28b..d8bbaba69a 100644
> --- a/Documentation/config/rebase.txt
> +++ b/Documentation/config/rebase.txt
> @@ -24,7 +24,12 @@ rebase.autoStash::
>         Defaults to false.
>
>  rebase.updateRefs::
> -       If set to true enable `--update-refs` option by default.
> +       If set to true, enable the `--update-refs` option of
> +       linkgit:git-rebase[1] by default. When set to 'interactive',
> +       only enable `--update-refs` by default for interactive mode
> +       (equivalent to `--update-refs=interactive`).
> +       This option can be overridden by specifying any form of
> +       `--update-refs` on the command line.
>
>  rebase.missingCommitsCheck::
>         If set to "warn", git rebase -i will print a warning if some
> diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
> index b18cdbc023..ae6939588d 100644
> --- a/Documentation/git-rebase.txt
> +++ b/Documentation/git-rebase.txt
> @@ -647,12 +647,18 @@ rebase --continue` is invoked. Currently, you cannot pass
>
>  --update-refs::
>  --no-update-refs::
> +--update-refs=interactive::

Based on `git grep -e '--.*\[=' Documentation/git-*.txt`, I think this
should be more like

    --update-refs[=interactive]::
    --no-update-refs::

But maybe that unintentionally suggests that `=interactive` is the default?

>         Automatically force-update any branches that point to commits that
>         are being rebased. Any branches that are checked out in a worktree
>         are not updated in this way.
>  +
> +If `--update-refs=interactive` is specified, the behavior is equivalent to
> +`--update-refs` if the rebase is interactive and `--no-update-refs` otherwise.
> +(This is mainly useful as a configuration setting, although it might also be
> +of use in aliases.)
> ++
>  If the configuration variable `rebase.updateRefs` is set, then this option
> -can be used to override and disable this setting.
> +can be used to override or disable the configuration.
>  +
>  See also INCOMPATIBLE OPTIONS below.
>
> diff --git a/builtin/rebase.c b/builtin/rebase.c
> index 6c9eaf3788..57b456599b 100644
> --- a/builtin/rebase.c
> +++ b/builtin/rebase.c
> @@ -129,10 +129,17 @@ struct rebase_options {
>         int reschedule_failed_exec;
>         int reapply_cherry_picks;
>         int fork_point;
> -       int update_refs;
> +       // UPDATE_REFS_{UNKNOWN,NO,ALWAYS} numeric values must never
> +       // change as post-option-parsing code works with {,config_}update_refs
> +       // as if they were ints
> +       enum {
> +               UPDATE_REFS_UNKNOWN = -1,
> +               UPDATE_REFS_NO = 0,
> +               UPDATE_REFS_ALWAYS = 1,
> +               UPDATE_REFS_INTERACTIVE,
> +       } update_refs, config_update_refs;
>         int config_autosquash;
>         int config_rebase_merges;
> -       int config_update_refs;
>  };
>
>  #define REBASE_OPTIONS_INIT {                          \
> @@ -150,8 +157,8 @@ struct rebase_options {
>                 .autosquash = -1,                       \
>                 .rebase_merges = -1,                    \
>                 .config_rebase_merges = -1,             \
> -               .update_refs = -1,                      \
> -               .config_update_refs = -1,               \
> +               .update_refs = UPDATE_REFS_UNKNOWN,     \
> +               .config_update_refs = UPDATE_REFS_UNKNOWN, \
>                 .strategy_opts = STRING_LIST_INIT_NODUP,\
>         }
>
> @@ -412,6 +419,18 @@ static void imply_merge(struct rebase_options *opts, const char *option)
>         }
>  }
>
> +static int coerce_update_refs(const struct rebase_options *opts, int update_refs)
> +{
> +       /* coerce "=interactive" into "no" rather than "not set" when not interactive
> +        * this way, `git -c rebase.updateRefs=yes rebase --update-refs=interactive [without -i]`
> +        * will not inherit the "yes" from the config */
> +       if (update_refs == UPDATE_REFS_INTERACTIVE)
> +               return (opts->flags & REBASE_INTERACTIVE_EXPLICIT)
> +                      ? UPDATE_REFS_ALWAYS
> +                      : UPDATE_REFS_NO;
> +       return update_refs;
> +}
> +
>  /* Returns the filename prefixed by the state_dir */
>  static const char *state_dir_path(const char *filename, struct rebase_options *opts)
>  {
> @@ -779,6 +798,17 @@ static void parse_rebase_merges_value(struct rebase_options *options, const char
>                 die(_("Unknown rebase-merges mode: %s"), value);
>  }
>
> +static int parse_update_refs_value(const char *value, const char *desc)
> +{
> +       int v = git_parse_maybe_bool(value);
> +       if (v >= 0)
> +               return v ? UPDATE_REFS_ALWAYS : UPDATE_REFS_NO;
> +       else if (!strcmp("interactive", value))
> +               return UPDATE_REFS_INTERACTIVE;
> +
> +       die(_("bad %s value '%s'; valid values are boolean or \"interactive\""), desc, value);
> +}
> +
>  static int rebase_config(const char *var, const char *value,
>                          const struct config_context *ctx, void *data)
>  {
> @@ -821,7 +851,8 @@ static int rebase_config(const char *var, const char *value,
>         }
>
>         if (!strcmp(var, "rebase.updaterefs")) {
> -               opts->config_update_refs = git_config_bool(var, value);
> +               opts->config_update_refs = parse_update_refs_value(value,
> +                       "rebase.updateRefs");
>                 return 0;
>         }
>
> @@ -1042,6 +1073,19 @@ static int parse_opt_rebase_merges(const struct option *opt, const char *arg, in
>         return 0;
>  }
>
> +static int parse_opt_update_refs(const struct option *opt, const char *arg, int unset)
> +{
> +       struct rebase_options *options = opt->value;
> +
> +       if (arg)
> +               options->update_refs = parse_update_refs_value(arg,
> +                       "--update-refs");
> +       else
> +               options->update_refs = unset ? UPDATE_REFS_NO : UPDATE_REFS_ALWAYS;
> +
> +       return 0;
> +}
> +
>  static void NORETURN error_on_missing_default_upstream(void)
>  {
>         struct branch *current_branch = branch_get(NULL);
> @@ -1187,9 +1231,11 @@ int cmd_rebase(int argc,
>                 OPT_BOOL(0, "autosquash", &options.autosquash,
>                          N_("move commits that begin with "
>                             "squash!/fixup! under -i")),
> -               OPT_BOOL(0, "update-refs", &options.update_refs,
> -                        N_("update branches that point to commits "
> -                           "that are being rebased")),
> +               OPT_CALLBACK_F(0, "update-refs", &options,
> +                       N_("(bool|interactive)"),
> +                       N_("update branches that point to commits "
> +                          "that are being rebased"),
> +                       PARSE_OPT_OPTARG, parse_opt_update_refs),
>                 { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
>                         N_("GPG-sign commits"),
>                         PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
> @@ -1528,6 +1574,16 @@ int cmd_rebase(int argc,
>         if (isatty(2) && options.flags & REBASE_NO_QUIET)
>                 strbuf_addstr(&options.git_format_patch_opt, " --progress");
>
> +       /* coerce --update-refs=interactive into yes or no.
> +        * we do it here because there's just too much code below that handles
> +        * {,config_}update_refs in one way or another and modifying it to
> +        * account for the new state would be too invasive.
> +        * all further code uses {,config_}update_refs as a tristate. */
> +       options.update_refs =
> +               coerce_update_refs(&options, options.update_refs);
> +       options.config_update_refs =
> +               coerce_update_refs(&options, options.config_update_refs);
> +
>         if (options.git_am_opts.nr || options.type == REBASE_APPLY) {
>                 /* all am options except -q are compatible only with --apply */
>                 for (i = options.git_am_opts.nr - 1; i >= 0; i--)
> --
> 2.48.1.5.g9188e14f140
>
>

Should we add a test for this?
Ivan Shapovalov Feb. 11, 2025, 11:33 a.m. UTC | #2
On 2025-02-10 at 15:22 -0500, D. Ben Knoble wrote:
> On Mon, Feb 10, 2025 at 2:17 PM Ivan Shapovalov <intelfx@intelfx.name> wrote:
> > 
> > In rebase-heavy workflows involving multiple interdependent feature
> > branches, typing out `--update-refs` quickly becomes tiring, which
> > can be mitigated with setting the `rebase.updateRefs` git-config option
> > to perform update-refs by default.
> > 
> > However, the utility of `rebase.updateRefs` is somewhat limited because
> > you rarely want it in a non-interactive rebase (as it does not give you
> > the chance to review the update-refs candidates, likely leading to
> > updating refs that you didn't want updated -- I made quite an amount
> > of mess by setting this option and subsequently forgetting about it).
> > 
> > Try to find a middle ground by introducing a third value,
> > `--update-refs=interactive` (and `rebase.updateRefs=interactive`)
> > which means `--update-refs` when starting an interactive rebase and
> > `--no-update-refs` otherwise. This option is primarily intended to be
> > used in the gitconfig, but is also accepted on the command line
> > for completeness.
> > 
> > Signed-off-by: Ivan Shapovalov <intelfx@intelfx.name>
> > ---
> >  Documentation/config/rebase.txt |  7 +++-
> >  Documentation/git-rebase.txt    |  8 +++-
> >  builtin/rebase.c                | 72 +++++++++++++++++++++++++++++----
> >  3 files changed, 77 insertions(+), 10 deletions(-)
> > 
> > diff --git a/Documentation/config/rebase.txt b/Documentation/config/rebase.txt
> > index c6187ab28b..d8bbaba69a 100644
> > --- a/Documentation/config/rebase.txt
> > +++ b/Documentation/config/rebase.txt
> > @@ -24,7 +24,12 @@ rebase.autoStash::
> >         Defaults to false.
> > 
> >  rebase.updateRefs::
> > -       If set to true enable `--update-refs` option by default.
> > +       If set to true, enable the `--update-refs` option of
> > +       linkgit:git-rebase[1] by default. When set to 'interactive',
> > +       only enable `--update-refs` by default for interactive mode
> > +       (equivalent to `--update-refs=interactive`).
> > +       This option can be overridden by specifying any form of
> > +       `--update-refs` on the command line.
> > 
> >  rebase.missingCommitsCheck::
> >         If set to "warn", git rebase -i will print a warning if some
> > diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
> > index b18cdbc023..ae6939588d 100644
> > --- a/Documentation/git-rebase.txt
> > +++ b/Documentation/git-rebase.txt
> > @@ -647,12 +647,18 @@ rebase --continue` is invoked. Currently, you cannot pass
> > 
> >  --update-refs::
> >  --no-update-refs::
> > +--update-refs=interactive::
> 
> Based on `git grep -e '--.*\[=' Documentation/git-*.txt`, I think this
> should be more like
> 
>     --update-refs[=interactive]::
>     --no-update-refs::
> 
> But maybe that unintentionally suggests that `=interactive` is the default?

Perhaps --update-refs[=(yes|no|interactive)] then? Or is that too
verbose? Anyway, I don't have a preference, I'll just do what I'm told
here.

> 
> >         Automatically force-update any branches that point to commits that
> >         are being rebased. Any branches that are checked out in a worktree
> >         are not updated in this way.
> >  +
> > +If `--update-refs=interactive` is specified, the behavior is equivalent to
> > +`--update-refs` if the rebase is interactive and `--no-update-refs` otherwise.
> > +(This is mainly useful as a configuration setting, although it might also be
> > +of use in aliases.)
> > ++
> >  If the configuration variable `rebase.updateRefs` is set, then this option
> > -can be used to override and disable this setting.
> > +can be used to override or disable the configuration.
> >  +
> >  See also INCOMPATIBLE OPTIONS below.
> > 
> > diff --git a/builtin/rebase.c b/builtin/rebase.c
> > index 6c9eaf3788..57b456599b 100644
> > --- a/builtin/rebase.c
> > +++ b/builtin/rebase.c
> > @@ -129,10 +129,17 @@ struct rebase_options {
> >         int reschedule_failed_exec;
> >         int reapply_cherry_picks;
> >         int fork_point;
> > -       int update_refs;
> > +       // UPDATE_REFS_{UNKNOWN,NO,ALWAYS} numeric values must never
> > +       // change as post-option-parsing code works with {,config_}update_refs
> > +       // as if they were ints
> > +       enum {
> > +               UPDATE_REFS_UNKNOWN = -1,
> > +               UPDATE_REFS_NO = 0,
> > +               UPDATE_REFS_ALWAYS = 1,
> > +               UPDATE_REFS_INTERACTIVE,
> > +       } update_refs, config_update_refs;
> >         int config_autosquash;
> >         int config_rebase_merges;
> > -       int config_update_refs;
> >  };
> > 
> >  #define REBASE_OPTIONS_INIT {                          \
> > @@ -150,8 +157,8 @@ struct rebase_options {
> >                 .autosquash = -1,                       \
> >                 .rebase_merges = -1,                    \
> >                 .config_rebase_merges = -1,             \
> > -               .update_refs = -1,                      \
> > -               .config_update_refs = -1,               \
> > +               .update_refs = UPDATE_REFS_UNKNOWN,     \
> > +               .config_update_refs = UPDATE_REFS_UNKNOWN, \
> >                 .strategy_opts = STRING_LIST_INIT_NODUP,\
> >         }
> > 
> > @@ -412,6 +419,18 @@ static void imply_merge(struct rebase_options *opts, const char *option)
> >         }
> >  }
> > 
> > +static int coerce_update_refs(const struct rebase_options *opts, int update_refs)
> > +{
> > +       /* coerce "=interactive" into "no" rather than "not set" when not interactive
> > +        * this way, `git -c rebase.updateRefs=yes rebase --update-refs=interactive [without -i]`
> > +        * will not inherit the "yes" from the config */
> > +       if (update_refs == UPDATE_REFS_INTERACTIVE)
> > +               return (opts->flags & REBASE_INTERACTIVE_EXPLICIT)
> > +                      ? UPDATE_REFS_ALWAYS
> > +                      : UPDATE_REFS_NO;
> > +       return update_refs;
> > +}
> > +
> >  /* Returns the filename prefixed by the state_dir */
> >  static const char *state_dir_path(const char *filename, struct rebase_options *opts)
> >  {
> > @@ -779,6 +798,17 @@ static void parse_rebase_merges_value(struct rebase_options *options, const char
> >                 die(_("Unknown rebase-merges mode: %s"), value);
> >  }
> > 
> > +static int parse_update_refs_value(const char *value, const char *desc)
> > +{
> > +       int v = git_parse_maybe_bool(value);
> > +       if (v >= 0)
> > +               return v ? UPDATE_REFS_ALWAYS : UPDATE_REFS_NO;
> > +       else if (!strcmp("interactive", value))
> > +               return UPDATE_REFS_INTERACTIVE;
> > +
> > +       die(_("bad %s value '%s'; valid values are boolean or \"interactive\""), desc, value);
> > +}
> > +
> >  static int rebase_config(const char *var, const char *value,
> >                          const struct config_context *ctx, void *data)
> >  {
> > @@ -821,7 +851,8 @@ static int rebase_config(const char *var, const char *value,
> >         }
> > 
> >         if (!strcmp(var, "rebase.updaterefs")) {
> > -               opts->config_update_refs = git_config_bool(var, value);
> > +               opts->config_update_refs = parse_update_refs_value(value,
> > +                       "rebase.updateRefs");
> >                 return 0;
> >         }
> > 
> > @@ -1042,6 +1073,19 @@ static int parse_opt_rebase_merges(const struct option *opt, const char *arg, in
> >         return 0;
> >  }
> > 
> > +static int parse_opt_update_refs(const struct option *opt, const char *arg, int unset)
> > +{
> > +       struct rebase_options *options = opt->value;
> > +
> > +       if (arg)
> > +               options->update_refs = parse_update_refs_value(arg,
> > +                       "--update-refs");
> > +       else
> > +               options->update_refs = unset ? UPDATE_REFS_NO : UPDATE_REFS_ALWAYS;
> > +
> > +       return 0;
> > +}
> > +
> >  static void NORETURN error_on_missing_default_upstream(void)
> >  {
> >         struct branch *current_branch = branch_get(NULL);
> > @@ -1187,9 +1231,11 @@ int cmd_rebase(int argc,
> >                 OPT_BOOL(0, "autosquash", &options.autosquash,
> >                          N_("move commits that begin with "
> >                             "squash!/fixup! under -i")),
> > -               OPT_BOOL(0, "update-refs", &options.update_refs,
> > -                        N_("update branches that point to commits "
> > -                           "that are being rebased")),
> > +               OPT_CALLBACK_F(0, "update-refs", &options,
> > +                       N_("(bool|interactive)"),
> > +                       N_("update branches that point to commits "
> > +                          "that are being rebased"),
> > +                       PARSE_OPT_OPTARG, parse_opt_update_refs),
> >                 { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
> >                         N_("GPG-sign commits"),
> >                         PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
> > @@ -1528,6 +1574,16 @@ int cmd_rebase(int argc,
> >         if (isatty(2) && options.flags & REBASE_NO_QUIET)
> >                 strbuf_addstr(&options.git_format_patch_opt, " --progress");
> > 
> > +       /* coerce --update-refs=interactive into yes or no.
> > +        * we do it here because there's just too much code below that handles
> > +        * {,config_}update_refs in one way or another and modifying it to
> > +        * account for the new state would be too invasive.
> > +        * all further code uses {,config_}update_refs as a tristate. */
> > +       options.update_refs =
> > +               coerce_update_refs(&options, options.update_refs);
> > +       options.config_update_refs =
> > +               coerce_update_refs(&options, options.config_update_refs);
> > +
> >         if (options.git_am_opts.nr || options.type == REBASE_APPLY) {
> >                 /* all am options except -q are compatible only with --apply */
> >                 for (i = options.git_am_opts.nr - 1; i >= 0; i--)
> > --
> > 2.48.1.5.g9188e14f140
> > 
> > 
> 
> Should we add a test for this?
> 

Any suggestions what exactly I should test here? I don't have much
experience testing interactive CLI tools, so I'd appreciate some hints.
Phillip Wood Feb. 11, 2025, 2:36 p.m. UTC | #3
Hi Ivan

On 10/02/2025 19:16, Ivan Shapovalov wrote:
> In rebase-heavy workflows involving multiple interdependent feature
> branches, typing out `--update-refs` quickly becomes tiring, which
> can be mitigated with setting the `rebase.updateRefs` git-config option
> to perform update-refs by default.
> 
> However, the utility of `rebase.updateRefs` is somewhat limited because
> you rarely want it in a non-interactive rebase (as it does not give you
> the chance to review the update-refs candidates, likely leading to
> updating refs that you didn't want updated -- I made quite an amount
> of mess by setting this option and subsequently forgetting about it).

I'm a bit surprised by this - I'd have thought there is more scope for 
messing things up by making a mistake when editing the todo list that 
for the non-interactive case. Are you able to explain a in a bit more 
detail the problem you have been experiencing please?

> Try to find a middle ground by introducing a third value,
> `--update-refs=interactive` (and `rebase.updateRefs=interactive`)
> which means `--update-refs` when starting an interactive rebase and
> `--no-update-refs` otherwise. This option is primarily intended to be
> used in the gitconfig, but is also accepted on the command line
> for completeness.

I'm not convinced allowing "--update-refs=interactive" on the 
commandline improves the usability - why wouldn't I just say 
"--update-refs" if I want to update all the branches or 
"--no-update-refs" if I don't? I also think supporting 
--update-refs=(true|false) is verbose and unnecessary as the user can 
already specify their intent with the existing option.

>   rebase.updateRefs::
> -	If set to true enable `--update-refs` option by default.
> +	If set to true, enable the `--update-refs` option of
> +	linkgit:git-rebase[1] by default. When set to 'interactive',

Our existing documentation is inconsistent in how it formats config 
values. rebase.backend uses "apply", rebase.rebaseMerges uses 
`rebase-cousins` which I think matches other commands and is therefore 
what we should use here and rebase.missingCommitCheck uses a mixture 
with "warn" and `drop`.

> +	only enable `--update-refs` by default for interactive mode
> +	(equivalent to `--update-refs=interactive`).
> +	This option can be overridden by specifying any form of
> +	`--update-refs` on the command line.

> @@ -129,10 +129,17 @@ struct rebase_options {
>   	int reschedule_failed_exec;
>   	int reapply_cherry_picks;
>   	int fork_point;
> -	int update_refs;
> +	// UPDATE_REFS_{UNKNOWN,NO,ALWAYS} numeric values must never
> +	// change as post-option-parsing code works with {,config_}update_refs
> +	// as if they were ints

This feels a bit fragile - why can't we update the code to use the enum? 
Also note that comments should be formatted as

/* single line comment */

or

/*
  * multi-line
  * comment
  */

> +	enum {
> +		UPDATE_REFS_UNKNOWN = -1,
> +		UPDATE_REFS_NO = 0,
> +		UPDATE_REFS_ALWAYS = 1,
> +		UPDATE_REFS_INTERACTIVE,
> +	} update_refs, config_update_refs;

I don't think we want to change the type of `update_refs` as I'm not 
convinced we want to change the commandline option.

> +static int coerce_update_refs(const struct rebase_options *opts, int update_refs)

I'd be tempted to call this "should_update_refs(...)"

> +{
> +	/* coerce "=interactive" into "no" rather than "not set" when not interactive
> +	 * this way, `git -c rebase.updateRefs=yes rebase --update-refs=interactive [without -i]`
> +	 * will not inherit the "yes" from the config */

Style - see above

> +	if (update_refs == UPDATE_REFS_INTERACTIVE)
> +		return (opts->flags & REBASE_INTERACTIVE_EXPLICIT)
> +		       ? UPDATE_REFS_ALWAYS
> +		       : UPDATE_REFS_NO;
> +	return update_refs;
> +}
> [...]   
> +static int parse_update_refs_value(const char *value, const char *desc)
> +{
> +	int v = git_parse_maybe_bool(value);

Style: there should be a blank line after the variable declarations at 
the start of a function.

> +	if (v >= 0)
> +		return v ? UPDATE_REFS_ALWAYS : UPDATE_REFS_NO;
> +	else if (!strcmp("interactive", value))
> +		return UPDATE_REFS_INTERACTIVE;
> +
> +	die(_("bad %s value '%s'; valid values are boolean or \"interactive\""), desc, value);

I think we normally say "invalid" or "unknown" rather than "bad" in our 
error messages. It'd be clearer just to list the possible values as 
there are only three of them.

> +	/* coerce --update-refs=interactive into yes or no.
> +	 * we do it here because there's just too much code below that handles
> +	 * {,config_}update_refs in one way or another and modifying it to
> +	 * account for the new state would be too invasive.
> +	 * all further code uses {,config_}update_refs as a tristate. */

I think we need to find a cleaner way of handling this. There are only 
two mentions of options.config_update_refs below this point - is it 
really so difficult for those to use the enum?

Given a bit more detail I could be convinced that the config option is 
useful but I don't think we should be changing the commandline option.

Best Wishes

Phillip
Junio C Hamano Feb. 11, 2025, 4:50 p.m. UTC | #4
Ivan Shapovalov <intelfx@intelfx.name> writes:

>> >  --update-refs::
>> >  --no-update-refs::
>> > +--update-refs=interactive::
>> 
>> Based on `git grep -e '--.*\[=' Documentation/git-*.txt`, I think this
>> should be more like
>> 
>>     --update-refs[=interactive]::
>>     --no-update-refs::
>> 
>> But maybe that unintentionally suggests that `=interactive` is the default?
>
> Perhaps --update-refs[=(yes|no|interactive)] then? Or is that too
> verbose?

If `--update-refs` does take values that the git_parse_maybe_bool()
helper parses as a Boolean value, I do not think the above is
verbose at all.  Rather, it is a disservice to the users if the
documentation does not mention yes/no in such a case.  I'd say
listing other Boolean synonyms like yes/true/on/no/false/off is
too verbose, though ;-).

> Anyway, I don't have a preference, I'll just do what I'm told

That is not quite in line with how we'd like to operate.

It is your itch.  Others may give suggestions to help you polish it,
but ultimately, we would not want to accept a patch that the author
does not agree with.

Thanks.
Ivan Shapovalov Feb. 11, 2025, 5:36 p.m. UTC | #5
On 2025-02-11 at 08:50 -0800, Junio C Hamano wrote:
> Ivan Shapovalov <intelfx@intelfx.name> writes:
> 
> > > >  --update-refs::
> > > >  --no-update-refs::
> > > > +--update-refs=interactive::
> > > 
> > > Based on `git grep -e '--.*\[=' Documentation/git-*.txt`, I think this
> > > should be more like
> > > 
> > >     --update-refs[=interactive]::
> > >     --no-update-refs::
> > > 
> > > But maybe that unintentionally suggests that `=interactive` is the default?
> > 
> > Perhaps --update-refs[=(yes|no|interactive)] then? Or is that too
> > verbose?
> 
> If `--update-refs` does take values that the git_parse_maybe_bool()
> helper parses as a Boolean value, I do not think the above is
> verbose at all.  Rather, it is a disservice to the users if the
> documentation does not mention yes/no in such a case.  I'd say
> listing other Boolean synonyms like yes/true/on/no/false/off is
> too verbose, though ;-).
> 
> > Anyway, I don't have a preference, I'll just do what I'm told
> 
> That is not quite in line with how we'd like to operate.
> 
> It is your itch.  Others may give suggestions to help you polish it,
> but ultimately, we would not want to accept a patch that the author
> does not agree with.

Of course, I care about the patch and the feature; what I wanted to say
is that I do not care (comparatively) about the formatting of the help
text: I couldn't figure it out on my own, so whatever you tell me is
the proper way of formatting it, I'll do.
Ivan Shapovalov Feb. 11, 2025, 6:11 p.m. UTC | #6
On 2025-02-11 at 14:36 +0000, Phillip Wood wrote:
> Hi Ivan
> 
> On 10/02/2025 19:16, Ivan Shapovalov wrote:
> > In rebase-heavy workflows involving multiple interdependent feature
> > branches, typing out `--update-refs` quickly becomes tiring, which
> > can be mitigated with setting the `rebase.updateRefs` git-config option
> > to perform update-refs by default.
> > 
> > However, the utility of `rebase.updateRefs` is somewhat limited because
> > you rarely want it in a non-interactive rebase (as it does not give you
> > the chance to review the update-refs candidates, likely leading to
> > updating refs that you didn't want updated -- I made quite an amount
> > of mess by setting this option and subsequently forgetting about it).
> 
> I'm a bit surprised by this - I'd have thought there is more scope for 
> messing things up by making a mistake when editing the todo list that 
> for the non-interactive case. Are you able to explain a in a bit more 
> detail the problem you have been experiencing please?

I often find myself managing multiple interdependent downstream patch
branches, rebasing them en masse from release to release. Eventually,
I found myself typing `git rebase -i --update-refs` more often than
not, so I just stuck it into the config as `rebase.updateRefs=true`.

However, sometimes I also maintain those patch branches for multiple
releases. Consider a (hypothetical) situation:

- tag v1
- tag v2
- branch work/myfeature-v1 that is based on tag v1

Now, I want to rebase myfeature onto v2, so I do this:

$ git checkout work/myfeature-v1
$ git checkout -b work/myfeature-v2
$ git rebase --onto v2 v1 work/myfeature-v2

With `rebase.updateRefs=true`, this ends up silently updating _both_
work/myfeature-v2 and work/myfeature-v1.

With this in mind, I wrote this patch such that update-refs only
happens for interactive rebases, when I have the chance to inspect the
todo list and prune unwanted update-refs items.

Does this make sense? I made an attempt to explain this motivation in
the commit message, so if this does make sense but the commit message
doesn't, please tell me how to improve/expand the latter.

> 
> > Try to find a middle ground by introducing a third value,
> > `--update-refs=interactive` (and `rebase.updateRefs=interactive`)
> > which means `--update-refs` when starting an interactive rebase and
> > `--no-update-refs` otherwise. This option is primarily intended to be
> > used in the gitconfig, but is also accepted on the command line
> > for completeness.
> 
> I'm not convinced allowing "--update-refs=interactive" on the 
> commandline improves the usability - why wouldn't I just say 
> "--update-refs" if I want to update all the branches or 
> "--no-update-refs" if I don't? I also think supporting 
> --update-refs=(true|false) is verbose and unnecessary as the user can 
> already specify their intent with the existing option.

I make heavy use of aliases for various workflows, which invoke one
another (making use of the ability to override earlier command-line
options with the latter ones), and the ability to spell out
`alias.myRebase = rebase ... --update-refs=interactive ...` was useful.

Re: specifying `=(true|false)`, the intention was to avoid unnecessary
divergence, both in UX and code (and reuse the parser to simplify said
code). If you think it will be harmful, I'll remove that.

> 
> >   rebase.updateRefs::
> > -	If set to true enable `--update-refs` option by default.
> > +	If set to true, enable the `--update-refs` option of
> > +	linkgit:git-rebase[1] by default. When set to 'interactive',
> 
> Our existing documentation is inconsistent in how it formats config 
> values. rebase.backend uses "apply", rebase.rebaseMerges uses 
> `rebase-cousins` which I think matches other commands and is therefore 
> what we should use here and rebase.missingCommitCheck uses a mixture 
> with "warn" and `drop`.

Apologies, I'm not sure I understood what exactly you were suggesting
here. Did you mean to suggest wrapping "interactive" in backticks
instead of single quotes?

> 
> > +	only enable `--update-refs` by default for interactive mode
> > +	(equivalent to `--update-refs=interactive`).
> > +	This option can be overridden by specifying any form of
> > +	`--update-refs` on the command line.
> 
> > @@ -129,10 +129,17 @@ struct rebase_options {
> >   	int reschedule_failed_exec;
> >   	int reapply_cherry_picks;
> >   	int fork_point;
> > -	int update_refs;
> > +	// UPDATE_REFS_{UNKNOWN,NO,ALWAYS} numeric values must never
> > +	// change as post-option-parsing code works with {,config_}update_refs
> > +	// as if they were ints
> 
> This feels a bit fragile - why can't we update the code to use the enum? 

It'd just be a lot of code to update, incl. stylistically (especially
the implicit truthy/falsy coercions or `>= 0`). I opted to make the
change as non-invasive as possible.

> Also note that comments should be formatted as
> 
> /* single line comment */
> 
> or
> 
> /*
>   * multi-line
>   * comment
>   */

OK

> 
> > +	enum {
> > +		UPDATE_REFS_UNKNOWN = -1,
> > +		UPDATE_REFS_NO = 0,
> > +		UPDATE_REFS_ALWAYS = 1,
> > +		UPDATE_REFS_INTERACTIVE,
> > +	} update_refs, config_update_refs;
> 
> I don't think we want to change the type of `update_refs` as I'm not 
> convinced we want to change the commandline option.
> 
> > +static int coerce_update_refs(const struct rebase_options *opts, int update_refs)
> 
> I'd be tempted to call this "should_update_refs(...)"

OK

> 
> > +{
> > +	/* coerce "=interactive" into "no" rather than "not set" when not interactive
> > +	 * this way, `git -c rebase.updateRefs=yes rebase --update-refs=interactive [without -i]`
> > +	 * will not inherit the "yes" from the config */
> 
> Style - see above

OK

> 
> > +	if (update_refs == UPDATE_REFS_INTERACTIVE)
> > +		return (opts->flags & REBASE_INTERACTIVE_EXPLICIT)
> > +		       ? UPDATE_REFS_ALWAYS
> > +		       : UPDATE_REFS_NO;
> > +	return update_refs;
> > +}
> > [...]   
> > +static int parse_update_refs_value(const char *value, const char *desc)
> > +{
> > +	int v = git_parse_maybe_bool(value);
> 
> Style: there should be a blank line after the variable declarations at 
> the start of a function.

OK

> 
> > +	if (v >= 0)
> > +		return v ? UPDATE_REFS_ALWAYS : UPDATE_REFS_NO;
> > +	else if (!strcmp("interactive", value))
> > +		return UPDATE_REFS_INTERACTIVE;
> > +
> > +	die(_("bad %s value '%s'; valid values are boolean or \"interactive\""), desc, value);
> 
> I think we normally say "invalid" or "unknown" rather than "bad" in our 
> error messages. It'd be clearer just to list the possible values as 
> there are only three of them.

It's not just three (see other review from Junio), otherwise OK

> 
> > +	/* coerce --update-refs=interactive into yes or no.
> > +	 * we do it here because there's just too much code below that handles
> > +	 * {,config_}update_refs in one way or another and modifying it to
> > +	 * account for the new state would be too invasive.
> > +	 * all further code uses {,config_}update_refs as a tristate. */
> 
> I think we need to find a cleaner way of handling this. There are only 
> two mentions of options.config_update_refs below this point - is it 
> really so difficult for those to use the enum?

See above; I opted to make this change as non-invasive as possible and
keep the complex argument validation logic (lines 1599, 1606-1609)
intact because I'm not even sure I understand it right.

Besides, even if I convert those uses to use enumerators, I still
wouldn't want to deal with non-tristate values beyond this point.
Ben Knoble Feb. 11, 2025, 7:28 p.m. UTC | #7
On Tue, Feb 11, 2025 at 6:33 AM Ivan Shapovalov <intelfx@intelfx.name> wrote:
>
> On 2025-02-10 at 15:22 -0500, D. Ben Knoble wrote:
> >
> > Based on `git grep -e '--.*\[=' Documentation/git-*.txt`, I think this
> > should be more like
> >
> >     --update-refs[=interactive]::
> >     --no-update-refs::
> >
> > But maybe that unintentionally suggests that `=interactive` is the default?
>
> Perhaps --update-refs[=(yes|no|interactive)] then? Or is that too
> verbose? Anyway, I don't have a preference, I'll just do what I'm told
> here.

I don't have a strong opinion, and I think this is being discussed
elsewhere in this thread.

> > Should we add a test for this?
> >
>
> Any suggestions what exactly I should test here? I don't have much
> experience testing interactive CLI tools, so I'd appreciate some hints.
>
> --
> Ivan Shapovalov / intelfx /

Give t/README a glance; t3404 is probably a good place to start given
"git grep update-refs t."
Ben Knoble Feb. 11, 2025, 7:29 p.m. UTC | #8
On Tue, Feb 11, 2025 at 2:28 PM D. Ben Knoble <ben.knoble@gmail.com> wrote:
>
> On Tue, Feb 11, 2025 at 6:33 AM Ivan Shapovalov <intelfx@intelfx.name> wrote:
> >
> > On 2025-02-10 at 15:22 -0500, D. Ben Knoble wrote:
> > > Should we add a test for this?
> > >
> >
> > Any suggestions what exactly I should test here? I don't have much
> > experience testing interactive CLI tools, so I'd appreciate some hints.
> >
> > --
> > Ivan Shapovalov / intelfx /
>
> Give t/README a glance; t3404 is probably a good place to start given
> "git grep update-refs t."

But I really meant to write—let's hash out the other details before
worrying about a test ;)
Phillip Wood Feb. 12, 2025, 2:26 p.m. UTC | #9
Hi Ivan

On 11/02/2025 18:11, Ivan Shapovalov wrote:
> On 2025-02-11 at 14:36 +0000, Phillip Wood wrote:
>> On 10/02/2025 19:16, Ivan Shapovalov wrote:
>>
>> I'm a bit surprised by this - I'd have thought there is more scope for
>> messing things up by making a mistake when editing the todo list that
>> for the non-interactive case. Are you able to explain a in a bit more
>> detail the problem you have been experiencing please?
> 
> I often find myself managing multiple interdependent downstream patch
> branches, rebasing them en masse from release to release. Eventually,
> I found myself typing `git rebase -i --update-refs` more often than
> not, so I just stuck it into the config as `rebase.updateRefs=true`.
> 
> However, sometimes I also maintain those patch branches for multiple
> releases. Consider a (hypothetical) situation:
> 
> - tag v1
> - tag v2
> - branch work/myfeature-v1 that is based on tag v1
> 
> Now, I want to rebase myfeature onto v2, so I do this:
> 
> $ git checkout work/myfeature-v1
> $ git checkout -b work/myfeature-v2
> $ git rebase --onto v2 v1 work/myfeature-v2
> 
> With `rebase.updateRefs=true`, this ends up silently updating _both_
> work/myfeature-v2 and work/myfeature-v1.

Thanks for the explanation. So this is about copying a branch and then 
rebasing the copy without updating the original. A while ago there was a 
discussion[1] about excluding branches that match HEAD from 
"--update-refs". Maybe we should revisit that with a view to adding a 
config setting that excludes copies of the current branch from 
"--update-refs".

Maintaining multiple versions of the same branch sounds like a lot of 
work - whats the advantage over merging a single branch into each release?

[1] 
https://lore.kernel.org/git/adb7f680-5bfa-6fa5-6d8a-61323fee7f53@haller-berlin.de/

> With this in mind, I wrote this patch such that update-refs only
> happens for interactive rebases, when I have the chance to inspect the
> todo list and prune unwanted update-refs items.
> 
> Does this make sense? I made an attempt to explain this motivation in
> the commit message, so if this does make sense but the commit message
> doesn't, please tell me how to improve/expand the latter.

I think having the example in the commit message would help - I feel 
like I've now got a clear idea of the problem you are facing whereas I 
didn't understand what the issue was just from the commit message.

>>> Try to find a middle ground by introducing a third value,
>>> `--update-refs=interactive` (and `rebase.updateRefs=interactive`)
>>> which means `--update-refs` when starting an interactive rebase and
>>> `--no-update-refs` otherwise. This option is primarily intended to be
>>> used in the gitconfig, but is also accepted on the command line
>>> for completeness.
>>
>> I'm not convinced allowing "--update-refs=interactive" on the
>> commandline improves the usability - why wouldn't I just say
>> "--update-refs" if I want to update all the branches or
>> "--no-update-refs" if I don't? I also think supporting
>> --update-refs=(true|false) is verbose and unnecessary as the user can
>> already specify their intent with the existing option.
> 
> I make heavy use of aliases for various workflows, which invoke one
> another (making use of the ability to override earlier command-line
> options with the latter ones), and the ability to spell out
> `alias.myRebase = rebase ... --update-refs=interactive ...` was useful.

You can write your alias as

    alias.myRebase = -c rebase.updaterefs=interactive rebase ...

instead. It is not quite as convenient but it means we don't have to add 
complexity to the command line interface that is only useful for aliases 
(I can't think of a use for "--update-refs=interactive" outside of an 
alias definition).

> Re: specifying `=(true|false)`, the intention was to avoid unnecessary
> divergence, both in UX and code (and reuse the parser to simplify said
> code). If you think it will be harmful, I'll remove that.

It would be even simpler if we didn't change the command line interface ;)

>>>    rebase.updateRefs::
>>> -	If set to true enable `--update-refs` option by default.
>>> +	If set to true, enable the `--update-refs` option of
>>> +	linkgit:git-rebase[1] by default. When set to 'interactive',
>>
>> Our existing documentation is inconsistent in how it formats config
>> values. rebase.backend uses "apply", rebase.rebaseMerges uses
>> `rebase-cousins` which I think matches other commands and is therefore
>> what we should use here and rebase.missingCommitCheck uses a mixture
>> with "warn" and `drop`.
> 
> Apologies, I'm not sure I understood what exactly you were suggesting
> here. Did you mean to suggest wrapping "interactive" in backticks
> instead of single quotes?

Sorry that wasn't very clear. Yes that is what I was trying to say.

>>> +	if (v >= 0)
>>> +		return v ? UPDATE_REFS_ALWAYS : UPDATE_REFS_NO;
>>> +	else if (!strcmp("interactive", value))
>>> +		return UPDATE_REFS_INTERACTIVE;
>>> +
>>> +	die(_("bad %s value '%s'; valid values are boolean or \"interactive\""), desc, value);
>>
>> I think we normally say "invalid" or "unknown" rather than "bad" in our
>> error messages. It'd be clearer just to list the possible values as
>> there are only three of them.
> 
> It's not just three (see other review from Junio), otherwise OK

As this is a hint in a error message I don't think we need to 
exhaustively list all the possible synonyms git accepts for "true" and 
"false"

>>> +	/* coerce --update-refs=interactive into yes or no.
>>> +	 * we do it here because there's just too much code below that handles
>>> +	 * {,config_}update_refs in one way or another and modifying it to
>>> +	 * account for the new state would be too invasive.
>>> +	 * all further code uses {,config_}update_refs as a tristate. */
>>
>> I think we need to find a cleaner way of handling this. There are only
>> two mentions of options.config_update_refs below this point - is it
>> really so difficult for those to use the enum?
> 
> See above; I opted to make this change as non-invasive as possible and
> keep the complex argument validation logic (lines 1599, 1606-1609)
> intact because I'm not even sure I understand it right.
> 
> Besides, even if I convert those uses to use enumerators, I still
> wouldn't want to deal with non-tristate values beyond this point.

We could add a new boolean variable which is initalized here and use 
that instead in the code below. Of the code below could just call 
should_update_refs() to convert the enum to a boolean.

Best Wishes

Phillip
Junio C Hamano Feb. 12, 2025, 4:58 p.m. UTC | #10
Phillip Wood <phillip.wood123@gmail.com> writes:

> Maintaining multiple versions of the same branch sounds like a lot of
> work - whats the advantage over merging a single branch into each
> release?

Making a single branch that would merge to each release track
cleanly, with preparing and maintaining semantic fixes necessary for
each track, is probably equally a lot of work, if not more.  I try
to do that for this project only because I am a perfectionist for
these things (and do so for fun), but I can understand if many
others (a pragmatist in me included) consider it not worth the
effort.  After all, it stops mattering once the branch finally gets
merged.
Ivan Shapovalov Feb. 12, 2025, 5:18 p.m. UTC | #11
On 2025-02-12 at 14:26 +0000, Phillip Wood wrote:
> Hi Ivan
> 
> On 11/02/2025 18:11, Ivan Shapovalov wrote:
> > On 2025-02-11 at 14:36 +0000, Phillip Wood wrote:
> > > On 10/02/2025 19:16, Ivan Shapovalov wrote:
> > > 
> > > I'm a bit surprised by this - I'd have thought there is more scope for
> > > messing things up by making a mistake when editing the todo list that
> > > for the non-interactive case. Are you able to explain a in a bit more
> > > detail the problem you have been experiencing please?
> > 
> > I often find myself managing multiple interdependent downstream patch
> > branches, rebasing them en masse from release to release. Eventually,
> > I found myself typing `git rebase -i --update-refs` more often than
> > not, so I just stuck it into the config as `rebase.updateRefs=true`.
> > 
> > However, sometimes I also maintain those patch branches for multiple
> > releases. Consider a (hypothetical) situation:
> > 
> > - tag v1
> > - tag v2
> > - branch work/myfeature-v1 that is based on tag v1
> > 
> > Now, I want to rebase myfeature onto v2, so I do this:
> > 
> > $ git checkout work/myfeature-v1
> > $ git checkout -b work/myfeature-v2
> > $ git rebase --onto v2 v1 work/myfeature-v2
> > 
> > With `rebase.updateRefs=true`, this ends up silently updating _both_
> > work/myfeature-v2 and work/myfeature-v1.
> 
> Thanks for the explanation. So this is about copying a branch and then 
> rebasing the copy without updating the original. A while ago there was a 
> discussion[1] about excluding branches that match HEAD from 
> "--update-refs". Maybe we should revisit that with a view to adding a 
> config setting that excludes copies of the current branch from 
> "--update-refs".

This idea stops working once you have a bunch of interdependent feature
branches (consider two branches work/myfeatureA and work/myfeatureB,
with the latter based on the former, with each having two versions as
described above, and then you rebase work/myfeatureB-v2 from v1 onto v2
and expect to update work/myfeatureA-v2 but not work/myfeatureA-v1).
Excluding branches that match HEAD is a very narrow workaround that
only fixes one particular instance of one particular workflow.

I don't understand the opposition, really — in my understanding, an
ability to restrict update-refs to interactive runs is a significantly
useful mechanism that does not impose any particular policy. It answers
the question of "I want git to _suggest_ updating refs by default, but
only if I have a chance to confirm/reject each particular update".

> 
> Maintaining multiple versions of the same branch sounds like a lot of 
> work - whats the advantage over merging a single branch into each release?

Different people, different workflows.
Phillip Wood Feb. 13, 2025, 9:43 a.m. UTC | #12
Hi Ivan

On 12/02/2025 17:18, Ivan Shapovalov wrote:
> On 2025-02-12 at 14:26 +0000, Phillip Wood wrote:
>>
>> Thanks for the explanation. So this is about copying a branch and then
>> rebasing the copy without updating the original. A while ago there was a
>> discussion[1] about excluding branches that match HEAD from
>> "--update-refs". Maybe we should revisit that with a view to adding a
>> config setting that excludes copies of the current branch from
>> "--update-refs".
> 
> This idea stops working once you have a bunch of interdependent feature
> branches (consider two branches work/myfeatureA and work/myfeatureB,
> with the latter based on the former, with each having two versions as
> described above, and then you rebase work/myfeatureB-v2 from v1 onto v2
> and expect to update work/myfeatureA-v2 but not work/myfeatureA-v1).
> Excluding branches that match HEAD is a very narrow workaround that
> only fixes one particular instance of one particular workflow.

Good point

> I don't understand the opposition, really — in my understanding, an
> ability to restrict update-refs to interactive runs is a significantly
> useful mechanism that does not impose any particular policy. It answers
> the question of "I want git to _suggest_ updating refs by default, but
> only if I have a chance to confirm/reject each particular update".

I'm not opposed, I'm just trying to understand the problem and see if 
there are synergies with other issues people have brought to the list in 
the past. You've convinced me that supporting 
"rebase.updateRefs=interactive" is worthwhile but I do not think we want 
to change the commandline interface. I'd much rather reserve the 
optional argument to support filtering in the future so that

    git rebase --update-refs='*-v2' --update-refs=^not-me-v2

would update all the branches ending in "-v2" except "not-me-v2". We'd 
want configure any default patterns separately to whether 
"--update-refs" was enabled by default which means we can add "rebase 
.updateRefs=interactive" without boxing ourselves into a corner.

>> Maintaining multiple versions of the same branch sounds like a lot of
>> work - whats the advantage over merging a single branch into each release?
> 
> Different people, different workflows.

Fair enough, from what Junio said it may actually be less work anyway.

Best Wishes

Phillip
Phillip Wood Feb. 13, 2025, 9:43 a.m. UTC | #13
On 12/02/2025 16:58, Junio C Hamano wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
> 
>> Maintaining multiple versions of the same branch sounds like a lot of
>> work - whats the advantage over merging a single branch into each
>> release?
> 
> Making a single branch that would merge to each release track
> cleanly, with preparing and maintaining semantic fixes necessary for
> each track, is probably equally a lot of work, if not more.  I try
> to do that for this project only because I am a perfectionist for
> these things (and do so for fun), but I can understand if many
> others (a pragmatist in me included) consider it not worth the
> effort.  After all, it stops mattering once the branch finally gets
> merged.

Thanks for that insight, I'd assumed the benefits of having a single 
source of truth for each branch outweighed the costs but it seems that 
is not necessarily the case.

Best Wishes

Phillip
Ivan Shapovalov Feb. 13, 2025, 12:04 p.m. UTC | #14
On 2025-02-13 at 09:43 +0000, phillip.wood123@gmail.com wrote:
> Hi Ivan
> 
> On 12/02/2025 17:18, Ivan Shapovalov wrote:
> > On 2025-02-12 at 14:26 +0000, Phillip Wood wrote:
> > > 
> > > Thanks for the explanation. So this is about copying a branch and then
> > > rebasing the copy without updating the original. A while ago there was a
> > > discussion[1] about excluding branches that match HEAD from
> > > "--update-refs". Maybe we should revisit that with a view to adding a
> > > config setting that excludes copies of the current branch from
> > > "--update-refs".
> > 
> > This idea stops working once you have a bunch of interdependent feature
> > branches (consider two branches work/myfeatureA and work/myfeatureB,
> > with the latter based on the former, with each having two versions as
> > described above, and then you rebase work/myfeatureB-v2 from v1 onto v2
> > and expect to update work/myfeatureA-v2 but not work/myfeatureA-v1).
> > Excluding branches that match HEAD is a very narrow workaround that
> > only fixes one particular instance of one particular workflow.
> 
> Good point
> 
> > I don't understand the opposition, really — in my understanding, an
> > ability to restrict update-refs to interactive runs is a significantly
> > useful mechanism that does not impose any particular policy. It answers
> > the question of "I want git to _suggest_ updating refs by default, but
> > only if I have a chance to confirm/reject each particular update".
> 
> I'm not opposed, I'm just trying to understand the problem and see if 
> there are synergies with other issues people have brought to the list in 
> the past. You've convinced me that supporting 
> "rebase.updateRefs=interactive" is worthwhile but I do not think we want 
> to change the commandline interface. I'd much rather reserve the 
> optional argument to support filtering in the future so that
> 
>     git rebase --update-refs='*-v2' --update-refs=^not-me-v2
> 
> would update all the branches ending in "-v2" except "not-me-v2". We'd 
> want configure any default patterns separately to whether 
> "--update-refs" was enabled by default which means we can add "rebase 
> .updateRefs=interactive" without boxing ourselves into a corner.

Makes sense, that's indeed a better use of the optional argument.
Alright, I'll send a v2 with +stylistic changes and -CLI changes.
Phillip Wood Feb. 19, 2025, 2:52 p.m. UTC | #15
Hi Ivan

On 13/02/2025 12:04, Ivan Shapovalov wrote:
>> would update all the branches ending in "-v2" except "not-me-v2". We'd
>> want configure any default patterns separately to whether
>> "--update-refs" was enabled by default which means we can add "rebase
>> .updateRefs=interactive" without boxing ourselves into a corner.
> 
> Makes sense, that's indeed a better use of the optional argument.
> Alright, I'll send a v2 with +stylistic changes and -CLI changes.

That's great, glad we're in agreement

Thanks

Phillip
diff mbox series

Patch

diff --git a/Documentation/config/rebase.txt b/Documentation/config/rebase.txt
index c6187ab28b..d8bbaba69a 100644
--- a/Documentation/config/rebase.txt
+++ b/Documentation/config/rebase.txt
@@ -24,7 +24,12 @@  rebase.autoStash::
 	Defaults to false.
 
 rebase.updateRefs::
-	If set to true enable `--update-refs` option by default.
+	If set to true, enable the `--update-refs` option of
+	linkgit:git-rebase[1] by default. When set to 'interactive',
+	only enable `--update-refs` by default for interactive mode
+	(equivalent to `--update-refs=interactive`).
+	This option can be overridden by specifying any form of
+	`--update-refs` on the command line.
 
 rebase.missingCommitsCheck::
 	If set to "warn", git rebase -i will print a warning if some
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index b18cdbc023..ae6939588d 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -647,12 +647,18 @@  rebase --continue` is invoked. Currently, you cannot pass
 
 --update-refs::
 --no-update-refs::
+--update-refs=interactive::
 	Automatically force-update any branches that point to commits that
 	are being rebased. Any branches that are checked out in a worktree
 	are not updated in this way.
 +
+If `--update-refs=interactive` is specified, the behavior is equivalent to
+`--update-refs` if the rebase is interactive and `--no-update-refs` otherwise.
+(This is mainly useful as a configuration setting, although it might also be
+of use in aliases.)
++
 If the configuration variable `rebase.updateRefs` is set, then this option
-can be used to override and disable this setting.
+can be used to override or disable the configuration.
 +
 See also INCOMPATIBLE OPTIONS below.
 
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 6c9eaf3788..57b456599b 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -129,10 +129,17 @@  struct rebase_options {
 	int reschedule_failed_exec;
 	int reapply_cherry_picks;
 	int fork_point;
-	int update_refs;
+	// UPDATE_REFS_{UNKNOWN,NO,ALWAYS} numeric values must never
+	// change as post-option-parsing code works with {,config_}update_refs
+	// as if they were ints
+	enum {
+		UPDATE_REFS_UNKNOWN = -1,
+		UPDATE_REFS_NO = 0,
+		UPDATE_REFS_ALWAYS = 1,
+		UPDATE_REFS_INTERACTIVE,
+	} update_refs, config_update_refs;
 	int config_autosquash;
 	int config_rebase_merges;
-	int config_update_refs;
 };
 
 #define REBASE_OPTIONS_INIT {			  	\
@@ -150,8 +157,8 @@  struct rebase_options {
 		.autosquash = -1,                       \
 		.rebase_merges = -1,                    \
 		.config_rebase_merges = -1,             \
-		.update_refs = -1,                      \
-		.config_update_refs = -1,               \
+		.update_refs = UPDATE_REFS_UNKNOWN,     \
+		.config_update_refs = UPDATE_REFS_UNKNOWN, \
 		.strategy_opts = STRING_LIST_INIT_NODUP,\
 	}
 
@@ -412,6 +419,18 @@  static void imply_merge(struct rebase_options *opts, const char *option)
 	}
 }
 
+static int coerce_update_refs(const struct rebase_options *opts, int update_refs)
+{
+	/* coerce "=interactive" into "no" rather than "not set" when not interactive
+	 * this way, `git -c rebase.updateRefs=yes rebase --update-refs=interactive [without -i]`
+	 * will not inherit the "yes" from the config */
+	if (update_refs == UPDATE_REFS_INTERACTIVE)
+		return (opts->flags & REBASE_INTERACTIVE_EXPLICIT)
+		       ? UPDATE_REFS_ALWAYS
+		       : UPDATE_REFS_NO;
+	return update_refs;
+}
+
 /* Returns the filename prefixed by the state_dir */
 static const char *state_dir_path(const char *filename, struct rebase_options *opts)
 {
@@ -779,6 +798,17 @@  static void parse_rebase_merges_value(struct rebase_options *options, const char
 		die(_("Unknown rebase-merges mode: %s"), value);
 }
 
+static int parse_update_refs_value(const char *value, const char *desc)
+{
+	int v = git_parse_maybe_bool(value);
+	if (v >= 0)
+		return v ? UPDATE_REFS_ALWAYS : UPDATE_REFS_NO;
+	else if (!strcmp("interactive", value))
+		return UPDATE_REFS_INTERACTIVE;
+
+	die(_("bad %s value '%s'; valid values are boolean or \"interactive\""), desc, value);
+}
+
 static int rebase_config(const char *var, const char *value,
 			 const struct config_context *ctx, void *data)
 {
@@ -821,7 +851,8 @@  static int rebase_config(const char *var, const char *value,
 	}
 
 	if (!strcmp(var, "rebase.updaterefs")) {
-		opts->config_update_refs = git_config_bool(var, value);
+		opts->config_update_refs = parse_update_refs_value(value,
+			"rebase.updateRefs");
 		return 0;
 	}
 
@@ -1042,6 +1073,19 @@  static int parse_opt_rebase_merges(const struct option *opt, const char *arg, in
 	return 0;
 }
 
+static int parse_opt_update_refs(const struct option *opt, const char *arg, int unset)
+{
+	struct rebase_options *options = opt->value;
+
+	if (arg)
+		options->update_refs = parse_update_refs_value(arg,
+			"--update-refs");
+	else
+		options->update_refs = unset ? UPDATE_REFS_NO : UPDATE_REFS_ALWAYS;
+
+	return 0;
+}
+
 static void NORETURN error_on_missing_default_upstream(void)
 {
 	struct branch *current_branch = branch_get(NULL);
@@ -1187,9 +1231,11 @@  int cmd_rebase(int argc,
 		OPT_BOOL(0, "autosquash", &options.autosquash,
 			 N_("move commits that begin with "
 			    "squash!/fixup! under -i")),
-		OPT_BOOL(0, "update-refs", &options.update_refs,
-			 N_("update branches that point to commits "
-			    "that are being rebased")),
+		OPT_CALLBACK_F(0, "update-refs", &options,
+			N_("(bool|interactive)"),
+			N_("update branches that point to commits "
+			   "that are being rebased"),
+			PARSE_OPT_OPTARG, parse_opt_update_refs),
 		{ OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
 			N_("GPG-sign commits"),
 			PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
@@ -1528,6 +1574,16 @@  int cmd_rebase(int argc,
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
+	/* coerce --update-refs=interactive into yes or no.
+	 * we do it here because there's just too much code below that handles
+	 * {,config_}update_refs in one way or another and modifying it to
+	 * account for the new state would be too invasive.
+	 * all further code uses {,config_}update_refs as a tristate. */
+	options.update_refs =
+		coerce_update_refs(&options, options.update_refs);
+	options.config_update_refs =
+		coerce_update_refs(&options, options.config_update_refs);
+
 	if (options.git_am_opts.nr || options.type == REBASE_APPLY) {
 		/* all am options except -q are compatible only with --apply */
 		for (i = options.git_am_opts.nr - 1; i >= 0; i--)