Merge branch 'kh/you-still-use-whatchanged-fix'

The "do you still use it?" message given by a command that is
deeply deprecated and allow us to suggest alternatives has been
updated.

* kh/you-still-use-whatchanged-fix:
  BreakingChanges: remove claim about whatchanged reports
  whatchanged: remove not-even-shorter clause
  whatchanged: hint about git-log(1) and aliasing
  you-still-use-that??: help the user help themselves
  t0014: test shadowing of aliases for a sample of builtins
  git: allow alias-shadowing deprecated builtins
  git: move seen-alias bookkeeping into handle_alias(...)
  git: add `deprecated` category to --list-cmds
  Makefile: don’t add whatchanged after it has been removed
main
Junio C Hamano 2025-10-02 12:26:12 -07:00
commit 7ae9eaf806
11 changed files with 165 additions and 46 deletions

View File

@ -241,7 +241,7 @@ These features will be removed.
equivalent `git log --raw`. We have nominated the command for equivalent `git log --raw`. We have nominated the command for
removal, have changed the command to refuse to work unless the removal, have changed the command to refuse to work unless the
`--i-still-use-this` option is given, and asked the users to report `--i-still-use-this` option is given, and asked the users to report
when they do so. So far there hasn't been a single complaint. when they do so.
+ +
The command will be removed. The command will be removed.



View File

@ -3,7 +3,8 @@ alias.*::
after defining `alias.last = cat-file commit HEAD`, the invocation after defining `alias.last = cat-file commit HEAD`, the invocation
`git last` is equivalent to `git cat-file commit HEAD`. To avoid `git last` is equivalent to `git cat-file commit HEAD`. To avoid
confusion and troubles with script usage, aliases that confusion and troubles with script usage, aliases that
hide existing Git commands are ignored. Arguments are split by hide existing Git commands are ignored except for deprecated
commands. Arguments are split by
spaces, the usual shell quoting and escaping are supported. spaces, the usual shell quoting and escaping are supported.
A quote pair or a backslash can be used to quote them. A quote pair or a backslash can be used to quote them.
+ +

View File

@ -15,7 +15,7 @@ WARNING
------- -------
`git whatchanged` has been deprecated and is scheduled for removal in `git whatchanged` has been deprecated and is scheduled for removal in
a future version of Git, as it is merely `git log` with different a future version of Git, as it is merely `git log` with different
default; `whatchanged` is not even shorter to type than `log --raw`. defaults.


DESCRIPTION DESCRIPTION
----------- -----------
@ -24,7 +24,11 @@ Shows commit logs and diff output each commit introduces.


New users are encouraged to use linkgit:git-log[1] instead. The New users are encouraged to use linkgit:git-log[1] instead. The
`whatchanged` command is essentially the same as linkgit:git-log[1] `whatchanged` command is essentially the same as linkgit:git-log[1]
but defaults to showing the raw format diff output and skipping merges. but defaults to showing the raw format diff output and skipping merges:

----
git log --raw --no-merges
----


The command is primarily kept for historical reasons; fingers of The command is primarily kept for historical reasons; fingers of
many people who learned Git long before `git log` was invented by many people who learned Git long before `git log` was invented by

View File

@ -219,7 +219,8 @@ If you just want to run git as if it was started in `<path>` then use
List commands by group. This is an internal/experimental List commands by group. This is an internal/experimental
option and may change or be removed in the future. Supported option and may change or be removed in the future. Supported
groups are: builtins, parseopt (builtin commands that use groups are: builtins, parseopt (builtin commands that use
parse-options), main (all commands in libexec directory), parse-options), deprecated (deprecated builtins),
main (all commands in libexec directory),
others (all other commands in `$PATH` that have git- prefix), others (all other commands in `$PATH` that have git- prefix),
list-<category> (see categories in command-list.txt), list-<category> (see categories in command-list.txt),
nohelpers (exclude helper commands), alias and config nohelpers (exclude helper commands), alias and config

View File

@ -883,7 +883,9 @@ BUILT_INS += git-stage$X
BUILT_INS += git-status$X BUILT_INS += git-status$X
BUILT_INS += git-switch$X BUILT_INS += git-switch$X
BUILT_INS += git-version$X BUILT_INS += git-version$X
ifndef WITH_BREAKING_CHANGES
BUILT_INS += git-whatchanged$X BUILT_INS += git-whatchanged$X
endif


# what 'all' will build but not install in gitexecdir # what 'all' will build but not install in gitexecdir
OTHER_PROGRAMS += git$X OTHER_PROGRAMS += git$X

View File

@ -543,7 +543,13 @@ int cmd_whatchanged(int argc,
cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg); cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);


if (!cfg.i_still_use_this) if (!cfg.i_still_use_this)
you_still_use_that("git whatchanged"); you_still_use_that("git whatchanged",
_("\n"
"hint: You can replace 'git whatchanged <opts>' with:\n"
"hint:\tgit log <opts> --raw --no-merges\n"
"hint: Or make an alias:\n"
"hint:\tgit config set --global alias.whatchanged 'log --raw --no-merges'\n"
"\n"));


if (!rev.diffopt.output_format) if (!rev.diffopt.output_format)
rev.diffopt.output_format = DIFF_FORMAT_RAW; rev.diffopt.output_format = DIFF_FORMAT_RAW;

View File

@ -626,7 +626,7 @@ int cmd_pack_redundant(int argc, const char **argv, const char *prefix UNUSED, s
} }


if (!i_still_use_this) if (!i_still_use_this)
you_still_use_that("git pack-redundant"); you_still_use_that("git pack-redundant", NULL);


if (load_all_packs) if (load_all_packs)
load_all(); load_all();

View File

@ -460,7 +460,7 @@ void warning_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));


void show_usage_if_asked(int ac, const char **av, const char *err); void show_usage_if_asked(int ac, const char **av, const char *err);


NORETURN void you_still_use_that(const char *command_name); NORETURN void you_still_use_that(const char *command_name, const char *hint);


#ifndef NO_OPENSSL #ifndef NO_OPENSSL
#ifdef APPLE_COMMON_CRYPTO #ifdef APPLE_COMMON_CRYPTO

91
git.c
View File

@ -28,6 +28,7 @@
#define NEED_WORK_TREE (1<<3) #define NEED_WORK_TREE (1<<3)
#define DELAY_PAGER_CONFIG (1<<4) #define DELAY_PAGER_CONFIG (1<<4)
#define NO_PARSEOPT (1<<5) /* parse-options is not used */ #define NO_PARSEOPT (1<<5) /* parse-options is not used */
#define DEPRECATED (1<<6)


struct cmd_struct { struct cmd_struct {
const char *cmd; const char *cmd;
@ -51,7 +52,9 @@ const char git_more_info_string[] =


static int use_pager = -1; static int use_pager = -1;


static void list_builtins(struct string_list *list, unsigned int exclude_option); static void list_builtins(struct string_list *list,
unsigned int include_option,
unsigned int exclude_option);


static void exclude_helpers_from_list(struct string_list *list) static void exclude_helpers_from_list(struct string_list *list)
{ {
@ -88,7 +91,7 @@ static int list_cmds(const char *spec)
int len = sep - spec; int len = sep - spec;


if (match_token(spec, len, "builtins")) if (match_token(spec, len, "builtins"))
list_builtins(&list, 0); list_builtins(&list, 0, 0);
else if (match_token(spec, len, "main")) else if (match_token(spec, len, "main"))
list_all_main_cmds(&list); list_all_main_cmds(&list);
else if (match_token(spec, len, "others")) else if (match_token(spec, len, "others"))
@ -99,6 +102,8 @@ static int list_cmds(const char *spec)
list_aliases(&list); list_aliases(&list);
else if (match_token(spec, len, "config")) else if (match_token(spec, len, "config"))
list_cmds_by_config(&list); list_cmds_by_config(&list);
else if (match_token(spec, len, "deprecated"))
list_builtins(&list, DEPRECATED, 0);
else if (len > 5 && !strncmp(spec, "list-", 5)) { else if (len > 5 && !strncmp(spec, "list-", 5)) {
struct strbuf sb = STRBUF_INIT; struct strbuf sb = STRBUF_INIT;


@ -322,7 +327,7 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
if (!strcmp(cmd, "parseopt")) { if (!strcmp(cmd, "parseopt")) {
struct string_list list = STRING_LIST_INIT_DUP; struct string_list list = STRING_LIST_INIT_DUP;


list_builtins(&list, NO_PARSEOPT); list_builtins(&list, 0, NO_PARSEOPT);
for (size_t i = 0; i < list.nr; i++) for (size_t i = 0; i < list.nr; i++)
printf("%s ", list.items[i].string); printf("%s ", list.items[i].string);
string_list_clear(&list, 0); string_list_clear(&list, 0);
@ -360,7 +365,7 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
return (*argv) - orig_argv; return (*argv) - orig_argv;
} }


static int handle_alias(struct strvec *args) static int handle_alias(struct strvec *args, struct string_list *expanded_aliases)
{ {
int envchanged = 0, ret = 0, saved_errno = errno; int envchanged = 0, ret = 0, saved_errno = errno;
int count, option_count; int count, option_count;
@ -371,6 +376,8 @@ static int handle_alias(struct strvec *args)
alias_command = args->v[0]; alias_command = args->v[0];
alias_string = alias_lookup(alias_command); alias_string = alias_lookup(alias_command);
if (alias_string) { if (alias_string) {
struct string_list_item *seen;

if (args->nr == 2 && !strcmp(args->v[1], "-h")) if (args->nr == 2 && !strcmp(args->v[1], "-h"))
fprintf_ln(stderr, _("'%s' is aliased to '%s'"), fprintf_ln(stderr, _("'%s' is aliased to '%s'"),
alias_command, alias_string); alias_command, alias_string);
@ -418,6 +425,25 @@ static int handle_alias(struct strvec *args)
if (!strcmp(alias_command, new_argv[0])) if (!strcmp(alias_command, new_argv[0]))
die(_("recursive alias: %s"), alias_command); die(_("recursive alias: %s"), alias_command);


string_list_append(expanded_aliases, alias_command);
seen = unsorted_string_list_lookup(expanded_aliases,
new_argv[0]);

if (seen) {
struct strbuf sb = STRBUF_INIT;
for (size_t i = 0; i < expanded_aliases->nr; i++) {
struct string_list_item *item = &expanded_aliases->items[i];

strbuf_addf(&sb, "\n %s", item->string);
if (item == seen)
strbuf_addstr(&sb, " <==");
else if (i == expanded_aliases->nr - 1)
strbuf_addstr(&sb, " ==>");
}
die(_("alias loop detected: expansion of '%s' does"
" not terminate:%s"), expanded_aliases->items[0].string, sb.buf);
}

trace_argv_printf(new_argv, trace_argv_printf(new_argv,
"trace: alias expansion: %s =>", "trace: alias expansion: %s =>",
alias_command); alias_command);
@ -591,7 +617,7 @@ static struct cmd_struct commands[] = {
{ "notes", cmd_notes, RUN_SETUP }, { "notes", cmd_notes, RUN_SETUP },
{ "pack-objects", cmd_pack_objects, RUN_SETUP }, { "pack-objects", cmd_pack_objects, RUN_SETUP },
#ifndef WITH_BREAKING_CHANGES #ifndef WITH_BREAKING_CHANGES
{ "pack-redundant", cmd_pack_redundant, RUN_SETUP | NO_PARSEOPT }, { "pack-redundant", cmd_pack_redundant, RUN_SETUP | NO_PARSEOPT | DEPRECATED },
#endif #endif
{ "pack-refs", cmd_pack_refs, RUN_SETUP }, { "pack-refs", cmd_pack_refs, RUN_SETUP },
{ "patch-id", cmd_patch_id, RUN_SETUP_GENTLY | NO_PARSEOPT }, { "patch-id", cmd_patch_id, RUN_SETUP_GENTLY | NO_PARSEOPT },
@ -649,7 +675,7 @@ static struct cmd_struct commands[] = {
{ "verify-tag", cmd_verify_tag, RUN_SETUP }, { "verify-tag", cmd_verify_tag, RUN_SETUP },
{ "version", cmd_version }, { "version", cmd_version },
#ifndef WITH_BREAKING_CHANGES #ifndef WITH_BREAKING_CHANGES
{ "whatchanged", cmd_whatchanged, RUN_SETUP }, { "whatchanged", cmd_whatchanged, RUN_SETUP | DEPRECATED },
#endif #endif
{ "worktree", cmd_worktree, RUN_SETUP }, { "worktree", cmd_worktree, RUN_SETUP },
{ "write-tree", cmd_write_tree, RUN_SETUP }, { "write-tree", cmd_write_tree, RUN_SETUP },
@ -670,11 +696,16 @@ int is_builtin(const char *s)
return !!get_builtin(s); return !!get_builtin(s);
} }


static void list_builtins(struct string_list *out, unsigned int exclude_option) static void list_builtins(struct string_list *out,
unsigned int include_option,
unsigned int exclude_option)
{ {
if (include_option && exclude_option)
BUG("'include_option' and 'exclude_option' are mutually exclusive");
for (size_t i = 0; i < ARRAY_SIZE(commands); i++) { for (size_t i = 0; i < ARRAY_SIZE(commands); i++) {
if (exclude_option && if (include_option && !(commands[i].option & include_option))
(commands[i].option & exclude_option)) continue;
if (exclude_option && (commands[i].option & exclude_option))
continue; continue;
string_list_append(out, commands[i].cmd); string_list_append(out, commands[i].cmd);
} }
@ -795,13 +826,29 @@ static void execv_dashed_external(const char **argv)
exit(128); exit(128);
} }


static int is_deprecated_command(const char *cmd)
{
struct cmd_struct *builtin = get_builtin(cmd);
return builtin && (builtin->option & DEPRECATED);
}

static int run_argv(struct strvec *args) static int run_argv(struct strvec *args)
{ {
int done_alias = 0; int done_alias = 0;
struct string_list cmd_list = STRING_LIST_INIT_DUP; struct string_list expanded_aliases = STRING_LIST_INIT_DUP;
struct string_list_item *seen;


while (1) { while (1) {
/*
* Allow deprecated commands to be overridden by aliases. This
* creates a seamless path forward for people who want to keep
* using the name after it is gone, but want to skip the
* deprecation complaint in the meantime.
*/
if (is_deprecated_command(args->v[0]) &&
handle_alias(args, &expanded_aliases)) {
done_alias = 1;
continue;
}
/* /*
* If we tried alias and futzed with our environment, * If we tried alias and futzed with our environment,
* it no longer is safe to invoke builtins directly in * it no longer is safe to invoke builtins directly in
@ -851,35 +898,17 @@ static int run_argv(struct strvec *args)
/* .. then try the external ones */ /* .. then try the external ones */
execv_dashed_external(args->v); execv_dashed_external(args->v);


seen = unsorted_string_list_lookup(&cmd_list, args->v[0]);
if (seen) {
struct strbuf sb = STRBUF_INIT;
for (size_t i = 0; i < cmd_list.nr; i++) {
struct string_list_item *item = &cmd_list.items[i];

strbuf_addf(&sb, "\n %s", item->string);
if (item == seen)
strbuf_addstr(&sb, " <==");
else if (i == cmd_list.nr - 1)
strbuf_addstr(&sb, " ==>");
}
die(_("alias loop detected: expansion of '%s' does"
" not terminate:%s"), cmd_list.items[0].string, sb.buf);
}

string_list_append(&cmd_list, args->v[0]);

/* /*
* It could be an alias -- this works around the insanity * It could be an alias -- this works around the insanity
* of overriding "git log" with "git show" by having * of overriding "git log" with "git show" by having
* alias.log = show * alias.log = show
*/ */
if (!handle_alias(args)) if (!handle_alias(args, &expanded_aliases))
break; break;
done_alias = 1; done_alias = 1;
} }


string_list_clear(&cmd_list, 0); string_list_clear(&expanded_aliases, 0);


return done_alias; return done_alias;
} }

View File

@ -27,6 +27,20 @@ test_expect_success 'looping aliases - internal execution' '
test_grep "^fatal: alias loop detected: expansion of" output test_grep "^fatal: alias loop detected: expansion of" output
' '


test_expect_success 'looping aliases - deprecated builtins' '
test_config alias.whatchanged pack-redundant &&
test_config alias.pack-redundant whatchanged &&
cat >expect <<-EOF &&
${SQ}whatchanged${SQ} is aliased to ${SQ}pack-redundant${SQ}
${SQ}pack-redundant${SQ} is aliased to ${SQ}whatchanged${SQ}
fatal: alias loop detected: expansion of ${SQ}whatchanged${SQ} does not terminate:
whatchanged <==
pack-redundant ==>
EOF
test_must_fail git whatchanged -h 2>actual &&
test_cmp expect actual
'

# This test is disabled until external loops are fixed, because would block # This test is disabled until external loops are fixed, because would block
# the test suite for a full minute. # the test suite for a full minute.
# #
@ -55,4 +69,47 @@ test_expect_success 'tracing a shell alias with arguments shows trace of prepare
test_cmp expect actual test_cmp expect actual
' '


can_alias_deprecated_builtin () {
cmd="$1" &&
# some git(1) commands will fail for `-h` (the case for
# git-status as of 2025-09-07)
test_might_fail git status -h >expect &&
test_file_not_empty expect &&
test_might_fail git -c alias."$cmd"=status "$cmd" -h >actual &&
test_cmp expect actual
}

test_expect_success 'can alias-shadow deprecated builtins' '
for cmd in $(git --list-cmds=deprecated)
do
can_alias_deprecated_builtin "$cmd" || return 1
done
'

test_expect_success 'can alias-shadow via two deprecated builtins' '
# some git(1) commands will fail... (see above)
test_might_fail git status -h >expect &&
test_file_not_empty expect &&
test_might_fail git -c alias.whatchanged=pack-redundant \
-c alias.pack-redundant=status whatchanged -h >actual &&
test_cmp expect actual
'

cannot_alias_regular_builtin () {
cmd="$1" &&
# some git(1) commands will fail... (see above)
test_might_fail git "$cmd" -h >expect &&
test_file_not_empty expect &&
test_might_fail git -c alias."$cmd"=status "$cmd" -h >actual &&
test_cmp expect actual
}

test_expect_success 'cannot alias-shadow a sample of regular builtins' '
for cmd in grep check-ref-format interpret-trailers \
checkout-index fast-import diagnose rev-list prune
do
cannot_alias_regular_builtin "$cmd" || return 1
done
'

test_done test_done

33
usage.c
View File

@ -7,6 +7,7 @@
#include "git-compat-util.h" #include "git-compat-util.h"
#include "gettext.h" #include "gettext.h"
#include "trace2.h" #include "trace2.h"
#include "strbuf.h"


static void vfreportf(FILE *f, const char *prefix, const char *err, va_list params) static void vfreportf(FILE *f, const char *prefix, const char *err, va_list params)
{ {
@ -376,14 +377,32 @@ void bug_fl(const char *file, int line, const char *fmt, ...)
va_end(ap); va_end(ap);
} }


NORETURN void you_still_use_that(const char *command_name)
NORETURN void you_still_use_that(const char *command_name, const char *hint)
{ {
struct strbuf percent_encoded = STRBUF_INIT;
strbuf_add_percentencode(&percent_encoded,
command_name,
STRBUF_ENCODE_SLASH);

fprintf(stderr, fprintf(stderr,
_("'%s' is nominated for removal.\n" _("'%s' is nominated for removal.\n"), command_name);
"If you still use this command, please add an extra\n"
"option, '--i-still-use-this', on the command line\n" if (hint)
"and let us know you still use it by sending an e-mail\n" fputs(hint, stderr);
"to <git@vger.kernel.org>. Thanks.\n"),
command_name); fprintf(stderr,
_("If you still use this command, here's what you can do:\n"
"\n"
"- read https://git-scm.com/docs/BreakingChanges.html\n"
"- check if anyone has discussed this on the mailing\n"
" list and if they came up with something that can\n"
" help you: https://lore.kernel.org/git/?q=%s\n"
"- send an email to <git@vger.kernel.org> to let us\n"
" know that you still use this command and were unable\n"
" to determine a suitable replacement\n"
"\n"),
percent_encoded.buf);
strbuf_release(&percent_encoded);
die(_("refusing to run without --i-still-use-this")); die(_("refusing to run without --i-still-use-this"));
} }