Git fork

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

+165 -46
+1 -1
Documentation/BreakingChanges.adoc
··· 241 equivalent `git log --raw`. We have nominated the command for 242 removal, have changed the command to refuse to work unless the 243 `--i-still-use-this` option is given, and asked the users to report 244 - when they do so. So far there hasn't been a single complaint. 245 + 246 The command will be removed. 247
··· 241 equivalent `git log --raw`. We have nominated the command for 242 removal, have changed the command to refuse to work unless the 243 `--i-still-use-this` option is given, and asked the users to report 244 + when they do so. 245 + 246 The command will be removed. 247
+2 -1
Documentation/config/alias.adoc
··· 3 after defining `alias.last = cat-file commit HEAD`, the invocation 4 `git last` is equivalent to `git cat-file commit HEAD`. To avoid 5 confusion and troubles with script usage, aliases that 6 - hide existing Git commands are ignored. Arguments are split by 7 spaces, the usual shell quoting and escaping are supported. 8 A quote pair or a backslash can be used to quote them. 9 +
··· 3 after defining `alias.last = cat-file commit HEAD`, the invocation 4 `git last` is equivalent to `git cat-file commit HEAD`. To avoid 5 confusion and troubles with script usage, aliases that 6 + hide existing Git commands are ignored except for deprecated 7 + commands. Arguments are split by 8 spaces, the usual shell quoting and escaping are supported. 9 A quote pair or a backslash can be used to quote them. 10 +
+6 -2
Documentation/git-whatchanged.adoc
··· 15 ------- 16 `git whatchanged` has been deprecated and is scheduled for removal in 17 a future version of Git, as it is merely `git log` with different 18 - default; `whatchanged` is not even shorter to type than `log --raw`. 19 20 DESCRIPTION 21 ----------- ··· 24 25 New users are encouraged to use linkgit:git-log[1] instead. The 26 `whatchanged` command is essentially the same as linkgit:git-log[1] 27 - but defaults to showing the raw format diff output and skipping merges. 28 29 The command is primarily kept for historical reasons; fingers of 30 many people who learned Git long before `git log` was invented by
··· 15 ------- 16 `git whatchanged` has been deprecated and is scheduled for removal in 17 a future version of Git, as it is merely `git log` with different 18 + defaults. 19 20 DESCRIPTION 21 ----------- ··· 24 25 New users are encouraged to use linkgit:git-log[1] instead. The 26 `whatchanged` command is essentially the same as linkgit:git-log[1] 27 + but defaults to showing the raw format diff output and skipping merges: 28 + 29 + ---- 30 + git log --raw --no-merges 31 + ---- 32 33 The command is primarily kept for historical reasons; fingers of 34 many people who learned Git long before `git log` was invented by
+2 -1
Documentation/git.adoc
··· 219 List commands by group. This is an internal/experimental 220 option and may change or be removed in the future. Supported 221 groups are: builtins, parseopt (builtin commands that use 222 - parse-options), main (all commands in libexec directory), 223 others (all other commands in `$PATH` that have git- prefix), 224 list-<category> (see categories in command-list.txt), 225 nohelpers (exclude helper commands), alias and config
··· 219 List commands by group. This is an internal/experimental 220 option and may change or be removed in the future. Supported 221 groups are: builtins, parseopt (builtin commands that use 222 + parse-options), deprecated (deprecated builtins), 223 + main (all commands in libexec directory), 224 others (all other commands in `$PATH` that have git- prefix), 225 list-<category> (see categories in command-list.txt), 226 nohelpers (exclude helper commands), alias and config
+2
Makefile
··· 883 BUILT_INS += git-status$X 884 BUILT_INS += git-switch$X 885 BUILT_INS += git-version$X 886 BUILT_INS += git-whatchanged$X 887 888 # what 'all' will build but not install in gitexecdir 889 OTHER_PROGRAMS += git$X
··· 883 BUILT_INS += git-status$X 884 BUILT_INS += git-switch$X 885 BUILT_INS += git-version$X 886 + ifndef WITH_BREAKING_CHANGES 887 BUILT_INS += git-whatchanged$X 888 + endif 889 890 # what 'all' will build but not install in gitexecdir 891 OTHER_PROGRAMS += git$X
+7 -1
builtin/log.c
··· 543 cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg); 544 545 if (!cfg.i_still_use_this) 546 - you_still_use_that("git whatchanged"); 547 548 if (!rev.diffopt.output_format) 549 rev.diffopt.output_format = DIFF_FORMAT_RAW;
··· 543 cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg); 544 545 if (!cfg.i_still_use_this) 546 + you_still_use_that("git whatchanged", 547 + _("\n" 548 + "hint: You can replace 'git whatchanged <opts>' with:\n" 549 + "hint:\tgit log <opts> --raw --no-merges\n" 550 + "hint: Or make an alias:\n" 551 + "hint:\tgit config set --global alias.whatchanged 'log --raw --no-merges'\n" 552 + "\n")); 553 554 if (!rev.diffopt.output_format) 555 rev.diffopt.output_format = DIFF_FORMAT_RAW;
+1 -1
builtin/pack-redundant.c
··· 626 } 627 628 if (!i_still_use_this) 629 - you_still_use_that("git pack-redundant"); 630 631 if (load_all_packs) 632 load_all();
··· 626 } 627 628 if (!i_still_use_this) 629 + you_still_use_that("git pack-redundant", NULL); 630 631 if (load_all_packs) 632 load_all();
+1 -1
git-compat-util.h
··· 460 461 void show_usage_if_asked(int ac, const char **av, const char *err); 462 463 - NORETURN void you_still_use_that(const char *command_name); 464 465 #ifndef NO_OPENSSL 466 #ifdef APPLE_COMMON_CRYPTO
··· 460 461 void show_usage_if_asked(int ac, const char **av, const char *err); 462 463 + NORETURN void you_still_use_that(const char *command_name, const char *hint); 464 465 #ifndef NO_OPENSSL 466 #ifdef APPLE_COMMON_CRYPTO
+60 -31
git.c
··· 28 #define NEED_WORK_TREE (1<<3) 29 #define DELAY_PAGER_CONFIG (1<<4) 30 #define NO_PARSEOPT (1<<5) /* parse-options is not used */ 31 32 struct cmd_struct { 33 const char *cmd; ··· 51 52 static int use_pager = -1; 53 54 - static void list_builtins(struct string_list *list, unsigned int exclude_option); 55 56 static void exclude_helpers_from_list(struct string_list *list) 57 { ··· 88 int len = sep - spec; 89 90 if (match_token(spec, len, "builtins")) 91 - list_builtins(&list, 0); 92 else if (match_token(spec, len, "main")) 93 list_all_main_cmds(&list); 94 else if (match_token(spec, len, "others")) ··· 99 list_aliases(&list); 100 else if (match_token(spec, len, "config")) 101 list_cmds_by_config(&list); 102 else if (len > 5 && !strncmp(spec, "list-", 5)) { 103 struct strbuf sb = STRBUF_INIT; 104 ··· 322 if (!strcmp(cmd, "parseopt")) { 323 struct string_list list = STRING_LIST_INIT_DUP; 324 325 - list_builtins(&list, NO_PARSEOPT); 326 for (size_t i = 0; i < list.nr; i++) 327 printf("%s ", list.items[i].string); 328 string_list_clear(&list, 0); ··· 360 return (*argv) - orig_argv; 361 } 362 363 - static int handle_alias(struct strvec *args) 364 { 365 int envchanged = 0, ret = 0, saved_errno = errno; 366 int count, option_count; ··· 371 alias_command = args->v[0]; 372 alias_string = alias_lookup(alias_command); 373 if (alias_string) { 374 if (args->nr == 2 && !strcmp(args->v[1], "-h")) 375 fprintf_ln(stderr, _("'%s' is aliased to '%s'"), 376 alias_command, alias_string); ··· 418 if (!strcmp(alias_command, new_argv[0])) 419 die(_("recursive alias: %s"), alias_command); 420 421 trace_argv_printf(new_argv, 422 "trace: alias expansion: %s =>", 423 alias_command); ··· 591 { "notes", cmd_notes, RUN_SETUP }, 592 { "pack-objects", cmd_pack_objects, RUN_SETUP }, 593 #ifndef WITH_BREAKING_CHANGES 594 - { "pack-redundant", cmd_pack_redundant, RUN_SETUP | NO_PARSEOPT }, 595 #endif 596 { "pack-refs", cmd_pack_refs, RUN_SETUP }, 597 { "patch-id", cmd_patch_id, RUN_SETUP_GENTLY | NO_PARSEOPT }, ··· 649 { "verify-tag", cmd_verify_tag, RUN_SETUP }, 650 { "version", cmd_version }, 651 #ifndef WITH_BREAKING_CHANGES 652 - { "whatchanged", cmd_whatchanged, RUN_SETUP }, 653 #endif 654 { "worktree", cmd_worktree, RUN_SETUP }, 655 { "write-tree", cmd_write_tree, RUN_SETUP }, ··· 670 return !!get_builtin(s); 671 } 672 673 - static void list_builtins(struct string_list *out, unsigned int exclude_option) 674 { 675 for (size_t i = 0; i < ARRAY_SIZE(commands); i++) { 676 - if (exclude_option && 677 - (commands[i].option & exclude_option)) 678 continue; 679 string_list_append(out, commands[i].cmd); 680 } ··· 795 exit(128); 796 } 797 798 static int run_argv(struct strvec *args) 799 { 800 int done_alias = 0; 801 - struct string_list cmd_list = STRING_LIST_INIT_DUP; 802 - struct string_list_item *seen; 803 804 while (1) { 805 /* 806 * If we tried alias and futzed with our environment, 807 * it no longer is safe to invoke builtins directly in ··· 851 /* .. then try the external ones */ 852 execv_dashed_external(args->v); 853 854 - seen = unsorted_string_list_lookup(&cmd_list, args->v[0]); 855 - if (seen) { 856 - struct strbuf sb = STRBUF_INIT; 857 - for (size_t i = 0; i < cmd_list.nr; i++) { 858 - struct string_list_item *item = &cmd_list.items[i]; 859 - 860 - strbuf_addf(&sb, "\n %s", item->string); 861 - if (item == seen) 862 - strbuf_addstr(&sb, " <=="); 863 - else if (i == cmd_list.nr - 1) 864 - strbuf_addstr(&sb, " ==>"); 865 - } 866 - die(_("alias loop detected: expansion of '%s' does" 867 - " not terminate:%s"), cmd_list.items[0].string, sb.buf); 868 - } 869 - 870 - string_list_append(&cmd_list, args->v[0]); 871 - 872 /* 873 * It could be an alias -- this works around the insanity 874 * of overriding "git log" with "git show" by having 875 * alias.log = show 876 */ 877 - if (!handle_alias(args)) 878 break; 879 done_alias = 1; 880 } 881 882 - string_list_clear(&cmd_list, 0); 883 884 return done_alias; 885 }
··· 28 #define NEED_WORK_TREE (1<<3) 29 #define DELAY_PAGER_CONFIG (1<<4) 30 #define NO_PARSEOPT (1<<5) /* parse-options is not used */ 31 + #define DEPRECATED (1<<6) 32 33 struct cmd_struct { 34 const char *cmd; ··· 52 53 static int use_pager = -1; 54 55 + static void list_builtins(struct string_list *list, 56 + unsigned int include_option, 57 + unsigned int exclude_option); 58 59 static void exclude_helpers_from_list(struct string_list *list) 60 { ··· 91 int len = sep - spec; 92 93 if (match_token(spec, len, "builtins")) 94 + list_builtins(&list, 0, 0); 95 else if (match_token(spec, len, "main")) 96 list_all_main_cmds(&list); 97 else if (match_token(spec, len, "others")) ··· 102 list_aliases(&list); 103 else if (match_token(spec, len, "config")) 104 list_cmds_by_config(&list); 105 + else if (match_token(spec, len, "deprecated")) 106 + list_builtins(&list, DEPRECATED, 0); 107 else if (len > 5 && !strncmp(spec, "list-", 5)) { 108 struct strbuf sb = STRBUF_INIT; 109 ··· 327 if (!strcmp(cmd, "parseopt")) { 328 struct string_list list = STRING_LIST_INIT_DUP; 329 330 + list_builtins(&list, 0, NO_PARSEOPT); 331 for (size_t i = 0; i < list.nr; i++) 332 printf("%s ", list.items[i].string); 333 string_list_clear(&list, 0); ··· 365 return (*argv) - orig_argv; 366 } 367 368 + static int handle_alias(struct strvec *args, struct string_list *expanded_aliases) 369 { 370 int envchanged = 0, ret = 0, saved_errno = errno; 371 int count, option_count; ··· 376 alias_command = args->v[0]; 377 alias_string = alias_lookup(alias_command); 378 if (alias_string) { 379 + struct string_list_item *seen; 380 + 381 if (args->nr == 2 && !strcmp(args->v[1], "-h")) 382 fprintf_ln(stderr, _("'%s' is aliased to '%s'"), 383 alias_command, alias_string); ··· 425 if (!strcmp(alias_command, new_argv[0])) 426 die(_("recursive alias: %s"), alias_command); 427 428 + string_list_append(expanded_aliases, alias_command); 429 + seen = unsorted_string_list_lookup(expanded_aliases, 430 + new_argv[0]); 431 + 432 + if (seen) { 433 + struct strbuf sb = STRBUF_INIT; 434 + for (size_t i = 0; i < expanded_aliases->nr; i++) { 435 + struct string_list_item *item = &expanded_aliases->items[i]; 436 + 437 + strbuf_addf(&sb, "\n %s", item->string); 438 + if (item == seen) 439 + strbuf_addstr(&sb, " <=="); 440 + else if (i == expanded_aliases->nr - 1) 441 + strbuf_addstr(&sb, " ==>"); 442 + } 443 + die(_("alias loop detected: expansion of '%s' does" 444 + " not terminate:%s"), expanded_aliases->items[0].string, sb.buf); 445 + } 446 + 447 trace_argv_printf(new_argv, 448 "trace: alias expansion: %s =>", 449 alias_command); ··· 617 { "notes", cmd_notes, RUN_SETUP }, 618 { "pack-objects", cmd_pack_objects, RUN_SETUP }, 619 #ifndef WITH_BREAKING_CHANGES 620 + { "pack-redundant", cmd_pack_redundant, RUN_SETUP | NO_PARSEOPT | DEPRECATED }, 621 #endif 622 { "pack-refs", cmd_pack_refs, RUN_SETUP }, 623 { "patch-id", cmd_patch_id, RUN_SETUP_GENTLY | NO_PARSEOPT }, ··· 675 { "verify-tag", cmd_verify_tag, RUN_SETUP }, 676 { "version", cmd_version }, 677 #ifndef WITH_BREAKING_CHANGES 678 + { "whatchanged", cmd_whatchanged, RUN_SETUP | DEPRECATED }, 679 #endif 680 { "worktree", cmd_worktree, RUN_SETUP }, 681 { "write-tree", cmd_write_tree, RUN_SETUP }, ··· 696 return !!get_builtin(s); 697 } 698 699 + static void list_builtins(struct string_list *out, 700 + unsigned int include_option, 701 + unsigned int exclude_option) 702 { 703 + if (include_option && exclude_option) 704 + BUG("'include_option' and 'exclude_option' are mutually exclusive"); 705 for (size_t i = 0; i < ARRAY_SIZE(commands); i++) { 706 + if (include_option && !(commands[i].option & include_option)) 707 + continue; 708 + if (exclude_option && (commands[i].option & exclude_option)) 709 continue; 710 string_list_append(out, commands[i].cmd); 711 } ··· 826 exit(128); 827 } 828 829 + static int is_deprecated_command(const char *cmd) 830 + { 831 + struct cmd_struct *builtin = get_builtin(cmd); 832 + return builtin && (builtin->option & DEPRECATED); 833 + } 834 + 835 static int run_argv(struct strvec *args) 836 { 837 int done_alias = 0; 838 + struct string_list expanded_aliases = STRING_LIST_INIT_DUP; 839 840 while (1) { 841 + /* 842 + * Allow deprecated commands to be overridden by aliases. This 843 + * creates a seamless path forward for people who want to keep 844 + * using the name after it is gone, but want to skip the 845 + * deprecation complaint in the meantime. 846 + */ 847 + if (is_deprecated_command(args->v[0]) && 848 + handle_alias(args, &expanded_aliases)) { 849 + done_alias = 1; 850 + continue; 851 + } 852 /* 853 * If we tried alias and futzed with our environment, 854 * it no longer is safe to invoke builtins directly in ··· 898 /* .. then try the external ones */ 899 execv_dashed_external(args->v); 900 901 /* 902 * It could be an alias -- this works around the insanity 903 * of overriding "git log" with "git show" by having 904 * alias.log = show 905 */ 906 + if (!handle_alias(args, &expanded_aliases)) 907 break; 908 done_alias = 1; 909 } 910 911 + string_list_clear(&expanded_aliases, 0); 912 913 return done_alias; 914 }
+57
t/t0014-alias.sh
··· 27 test_grep "^fatal: alias loop detected: expansion of" output 28 ' 29 30 # This test is disabled until external loops are fixed, because would block 31 # the test suite for a full minute. 32 # ··· 53 # redact platform differences 54 sed -n -e "s/^\(trace: start_command:\) .* -c /\1 SHELL -c /p" output >actual && 55 test_cmp expect actual 56 ' 57 58 test_done
··· 27 test_grep "^fatal: alias loop detected: expansion of" output 28 ' 29 30 + test_expect_success 'looping aliases - deprecated builtins' ' 31 + test_config alias.whatchanged pack-redundant && 32 + test_config alias.pack-redundant whatchanged && 33 + cat >expect <<-EOF && 34 + ${SQ}whatchanged${SQ} is aliased to ${SQ}pack-redundant${SQ} 35 + ${SQ}pack-redundant${SQ} is aliased to ${SQ}whatchanged${SQ} 36 + fatal: alias loop detected: expansion of ${SQ}whatchanged${SQ} does not terminate: 37 + whatchanged <== 38 + pack-redundant ==> 39 + EOF 40 + test_must_fail git whatchanged -h 2>actual && 41 + test_cmp expect actual 42 + ' 43 + 44 # This test is disabled until external loops are fixed, because would block 45 # the test suite for a full minute. 46 # ··· 67 # redact platform differences 68 sed -n -e "s/^\(trace: start_command:\) .* -c /\1 SHELL -c /p" output >actual && 69 test_cmp expect actual 70 + ' 71 + 72 + can_alias_deprecated_builtin () { 73 + cmd="$1" && 74 + # some git(1) commands will fail for `-h` (the case for 75 + # git-status as of 2025-09-07) 76 + test_might_fail git status -h >expect && 77 + test_file_not_empty expect && 78 + test_might_fail git -c alias."$cmd"=status "$cmd" -h >actual && 79 + test_cmp expect actual 80 + } 81 + 82 + test_expect_success 'can alias-shadow deprecated builtins' ' 83 + for cmd in $(git --list-cmds=deprecated) 84 + do 85 + can_alias_deprecated_builtin "$cmd" || return 1 86 + done 87 + ' 88 + 89 + test_expect_success 'can alias-shadow via two deprecated builtins' ' 90 + # some git(1) commands will fail... (see above) 91 + test_might_fail git status -h >expect && 92 + test_file_not_empty expect && 93 + test_might_fail git -c alias.whatchanged=pack-redundant \ 94 + -c alias.pack-redundant=status whatchanged -h >actual && 95 + test_cmp expect actual 96 + ' 97 + 98 + cannot_alias_regular_builtin () { 99 + cmd="$1" && 100 + # some git(1) commands will fail... (see above) 101 + test_might_fail git "$cmd" -h >expect && 102 + test_file_not_empty expect && 103 + test_might_fail git -c alias."$cmd"=status "$cmd" -h >actual && 104 + test_cmp expect actual 105 + } 106 + 107 + test_expect_success 'cannot alias-shadow a sample of regular builtins' ' 108 + for cmd in grep check-ref-format interpret-trailers \ 109 + checkout-index fast-import diagnose rev-list prune 110 + do 111 + cannot_alias_regular_builtin "$cmd" || return 1 112 + done 113 ' 114 115 test_done
+26 -7
usage.c
··· 7 #include "git-compat-util.h" 8 #include "gettext.h" 9 #include "trace2.h" 10 11 static void vfreportf(FILE *f, const char *prefix, const char *err, va_list params) 12 { ··· 376 va_end(ap); 377 } 378 379 - NORETURN void you_still_use_that(const char *command_name) 380 { 381 fprintf(stderr, 382 - _("'%s' is nominated for removal.\n" 383 - "If you still use this command, please add an extra\n" 384 - "option, '--i-still-use-this', on the command line\n" 385 - "and let us know you still use it by sending an e-mail\n" 386 - "to <git@vger.kernel.org>. Thanks.\n"), 387 - command_name); 388 die(_("refusing to run without --i-still-use-this")); 389 }
··· 7 #include "git-compat-util.h" 8 #include "gettext.h" 9 #include "trace2.h" 10 + #include "strbuf.h" 11 12 static void vfreportf(FILE *f, const char *prefix, const char *err, va_list params) 13 { ··· 377 va_end(ap); 378 } 379 380 + 381 + NORETURN void you_still_use_that(const char *command_name, const char *hint) 382 { 383 + struct strbuf percent_encoded = STRBUF_INIT; 384 + strbuf_add_percentencode(&percent_encoded, 385 + command_name, 386 + STRBUF_ENCODE_SLASH); 387 + 388 fprintf(stderr, 389 + _("'%s' is nominated for removal.\n"), command_name); 390 + 391 + if (hint) 392 + fputs(hint, stderr); 393 + 394 + fprintf(stderr, 395 + _("If you still use this command, here's what you can do:\n" 396 + "\n" 397 + "- read https://git-scm.com/docs/BreakingChanges.html\n" 398 + "- check if anyone has discussed this on the mailing\n" 399 + " list and if they came up with something that can\n" 400 + " help you: https://lore.kernel.org/git/?q=%s\n" 401 + "- send an email to <git@vger.kernel.org> to let us\n" 402 + " know that you still use this command and were unable\n" 403 + " to determine a suitable replacement\n" 404 + "\n"), 405 + percent_encoded.buf); 406 + strbuf_release(&percent_encoded); 407 die(_("refusing to run without --i-still-use-this")); 408 }