commit: print advice when core.commentString=auto

Add some advice on how to change the config settings when "core.commentString=auto" or "core.commentChar=auto". The advice includes instructions for clearing the config setting or setting a fixed comment string. To try and be as specific as possible, the advice is customized based on the user's config. If "core.commentString=auto" is set in the system config and the user does not have write access then the advice omits the instructions to clear the config and recommends changing the global config instead. An alternative approach would be to advise the user to run "git config --show-origin" and leave them to figure out how to fix it themselves but that seems rather unfriendly. As we're forcing them to update their config we should try and make that as easy as possible. In order to generate this advice we need to record each file where either of the config keys is set and whether a key occurs more that once in a given file. This lets us generate the list of commands to remove all the keys and also tells us which key the "auto" setting comes from. As we want the user to update their config we do not provide a way for this advice to be disabled other than changing the value of "core.commentChar" or "core.commentString". Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phillip Wood committed Aug 26, 2025 at 14:35 UTC ace1bb71503bc53b42ddfd68435c3af0adaf390f
3 files changed +233 -10
config.c
+188 -6
@@ -8,6 +8,7 @@
8
9 #include "git-compat-util.h"
10 #include "abspath.h"
11 +#include "advice.h"
12 #include "date.h"
13 #include "branch.h"
14 #include "config.h"
@@ -1955,9 +1956,51 @@ int git_configset_get_pathname(struct config_set *set, const char *key, char **d
1956 struct comment_char_config {
1957 unsigned last_key_id;
1958 bool auto_set;
1959 + bool auto_set_in_file;
1960 + struct strintmap key_flags;
1961 + size_t alloc, nr;
1962 + struct comment_char_config_item {
1963 + unsigned key_id;
1964 + char *path;
1965 + enum config_scope scope;
1966 + } *item;
1967 };
1968
1960 -#define COMMENT_CHAR_CFG_INIT { 0 }
1969 +#define COMMENT_CHAR_CFG_INIT { \
1970 + .key_flags = STRINTMAP_INIT, \
1971 + }
1972 +
1973 +static void comment_char_config_release(struct comment_char_config *config)
1974 +{
1975 + strintmap_clear(&config->key_flags);
1976 + for (size_t i = 0; i < config->nr; i++)
1977 + free(config->item[i].path);
1978 + free(config->item);
1979 +}
1980 +
1981 +/* Used to track whether the key occurs more than once in a given file */
1982 +#define KEY_SEEN_ONCE 1u
1983 +#define KEY_SEEN_TWICE 2u
1984 +#define COMMENT_KEY_SHIFT(id) (2 * (id))
1985 +#define COMMENT_KEY_MASK(id) (3u << COMMENT_KEY_SHIFT(id))
1986 +
1987 +static void set_comment_key_flags(struct comment_char_config *config,
1988 + const char *path, unsigned id, unsigned value)
1989 +{
1990 + unsigned old = strintmap_get(&config->key_flags, path);
1991 + unsigned new = (old & ~COMMENT_KEY_MASK(id)) |
1992 + value << COMMENT_KEY_SHIFT(id);
1993 +
1994 + strintmap_set(&config->key_flags, path, new);
1995 +}
1996 +
1997 +static unsigned get_comment_key_flags(struct comment_char_config *config,
1998 + const char *path, unsigned id)
1999 +{
2000 + unsigned value = strintmap_get(&config->key_flags, path);
2001 +
2002 + return (value & COMMENT_KEY_MASK(id)) >> COMMENT_KEY_SHIFT(id);
2003 +}
2004
2005 static const char *comment_key_name(unsigned id)
2006 {
@@ -1973,10 +2016,10 @@ static const char *comment_key_name(unsigned id)
2016 }
2017
2018 static void comment_char_callback(const char *key, const char *value,
1976 - const struct config_context *ctx UNUSED,
1977 - void *data)
2019 + const struct config_context *ctx, void *data)
2020 {
2021 struct comment_char_config *config = data;
2022 + const struct key_value_info *kvi = ctx->kvi;
2023 unsigned key_id;
2024
2025 if (!strcmp(key, "core.commentchar"))
@@ -1988,8 +2031,136 @@ static void comment_char_callback(const char *key, const char *value,
2031
2032 config->last_key_id = key_id;
2033 config->auto_set = value && !strcmp(value, "auto");
2034 + if (kvi->origin_type != CONFIG_ORIGIN_FILE) {
2035 + return;
2036 + } else if (get_comment_key_flags(config, kvi->filename, key_id)) {
2037 + set_comment_key_flags(config, kvi->filename, key_id,
2038 + KEY_SEEN_TWICE);
2039 + } else {
2040 + struct comment_char_config_item *item;
2041 +
2042 + ALLOC_GROW_BY(config->item, config->nr, 1, config->alloc);
2043 + item = &config->item[config->nr - 1];
2044 + item->key_id = key_id;
2045 + item->scope = kvi->scope;
2046 + item->path = xstrdup(kvi->filename);
2047 + set_comment_key_flags(config, kvi->filename, key_id,
2048 + KEY_SEEN_ONCE);
2049 + }
2050 + config->auto_set_in_file = config->auto_set;
2051 }
2052
2053 +static void add_config_scope_arg(struct repository *repo, struct strbuf *buf,
2054 + struct comment_char_config_item *item)
2055 +{
2056 + char *global_config = git_global_config();
2057 + char *system_config = git_system_config();
2058 +
2059 + if (item->scope == CONFIG_SCOPE_SYSTEM && access(item->path, W_OK)) {
2060 + /*
2061 + * If the user cannot write to the system config recommend
2062 + * setting the global config instead.
2063 + */
2064 + strbuf_addstr(buf, "--global ");
2065 + } else if (fspatheq(item->path, system_config)) {
2066 + strbuf_addstr(buf, "--system ");
2067 + } else if (fspatheq(item->path, global_config)) {
2068 + strbuf_addstr(buf, "--global ");
2069 + } else if (fspatheq(item->path,
2070 + mkpath("%s/config",
2071 + repo_get_git_dir(repo)))) {
2072 + ; /* --local is the default */
2073 + } else if (fspatheq(item->path,
2074 + mkpath("%s/config.worktree",
2075 + repo_get_common_dir(repo)))) {
2076 + strbuf_addstr(buf, "--worktree ");
2077 + } else {
2078 + const char *path = item->path;
2079 + const char *home = getenv("HOME");
2080 +
2081 + strbuf_addstr(buf, "--file ");
2082 + if (home && !fspathncmp(path, home, strlen(home))) {
2083 + path += strlen(home);
2084 + if (!fspathncmp(path, "/", 1))
2085 + path++;
2086 + strbuf_addstr(buf, "~/");
2087 + }
2088 + sq_quote_buf_pretty(buf, path);
2089 + strbuf_addch(buf, ' ');
2090 + }
2091 +
2092 + free(global_config);
2093 + free(system_config);
2094 +}
2095 +
2096 +static bool can_unset_comment_char_config(struct comment_char_config *config)
2097 +{
2098 + for (size_t i = 0; i < config->nr; i++) {
2099 + struct comment_char_config_item *item = &config->item[i];
2100 +
2101 + if (item->scope == CONFIG_SCOPE_SYSTEM &&
2102 + access(item->path, W_OK))
2103 + return false;
2104 + }
2105 +
2106 + return true;
2107 +}
2108 +
2109 +static void add_unset_auto_comment_char_advice(struct repository *repo,
2110 + struct comment_char_config *config)
2111 +{
2112 + struct strbuf buf = STRBUF_INIT;
2113 +
2114 + if (!can_unset_comment_char_config(config))
2115 + return;
2116 +
2117 + for (size_t i = 0; i < config->nr; i++) {
2118 + struct comment_char_config_item *item = &config->item[i];
2119 +
2120 + strbuf_addstr(&buf, " git config unset ");
2121 + add_config_scope_arg(repo, &buf, item);
2122 + if (get_comment_key_flags(config, item->path, item->key_id) == KEY_SEEN_TWICE)
2123 + strbuf_addstr(&buf, "--all ");
2124 + strbuf_addf(&buf, "%s\n", comment_key_name(item->key_id));
2125 + }
2126 + advise(_("\nTo use the default comment string (#) please run\n\n%s"),
2127 + buf.buf);
2128 + strbuf_release(&buf);
2129 +}
2130 +
2131 +static void add_comment_char_advice(struct repository *repo,
2132 + struct comment_char_config *config)
2133 +{
2134 + struct strbuf buf = STRBUF_INIT;
2135 + struct comment_char_config_item *item;
2136 + /* TRANSLATORS this is a place holder for the value of core.commentString */
2137 + const char *placeholder = _("<comment string>");
2138 +
2139 + /*
2140 + * If auto is set in the last file that we saw advise the user how to
2141 + * update their config.
2142 + */
2143 + if (!config->auto_set_in_file)
2144 + return;
2145 +
2146 + add_unset_auto_comment_char_advice(repo, config);
2147 + item = &config->item[config->nr - 1];
2148 + strbuf_reset(&buf);
2149 + strbuf_addstr(&buf, " git config set ");
2150 + add_config_scope_arg(repo, &buf, item);
2151 + strbuf_addf(&buf, "%s %s\n", comment_key_name(item->key_id),
2152 + placeholder);
2153 + advise(_("\nTo set a custom comment string please run\n\n"
2154 + "%s\nwhere '%s' is the string you wish to use.\n"),
2155 + buf.buf, placeholder);
2156 + strbuf_release(&buf);
2157 +}
2158 +
2159 +#undef KEY_SEEN_ONCE
2160 +#undef KEY_SEEN_TWICE
2161 +#undef COMMENT_KEY_SHIFT
2162 +#undef COMMENT_KEY_MASK
2163 +
2164 struct repo_config {
2165 struct repository *repo;
2166 struct comment_char_config comment_char_config;
@@ -2000,18 +2171,26 @@ struct repo_config {
2171 .repo = repo_, \
2172 };
2173
2174 +static void repo_config_release(struct repo_config *config)
2175 +{
2176 + comment_char_config_release(&config->comment_char_config);
2177 +}
2178 +
2179 #ifdef WITH_BREAKING_CHANGES
2004 -static void check_auto_comment_char_config(struct comment_char_config *config)
2180 +static void check_auto_comment_char_config(struct repository *repo,
2181 + struct comment_char_config *config)
2182 {
2183 if (!config->auto_set)
2184 return;
2185
2186 die_message(_("Support for '%s=auto' has been removed in Git 3.0"),
2187 comment_key_name(config->last_key_id));
2188 + add_comment_char_advice(repo, config);
2189 die(NULL);
2190 }
2191 #else
2014 -static void check_auto_comment_char_config(struct comment_char_config *config)
2192 +static void check_auto_comment_char_config(struct repository *repo,
2193 + struct comment_char_config *config)
2194 {
2195 extern bool warn_on_auto_comment_char;
2196 const char *DEPRECATED_CONFIG_ENV =
@@ -2031,6 +2210,7 @@ static void check_auto_comment_char_config(struct comment_char_config *config)
2210
2211 warning(_("Support for '%s=auto' is deprecated and will be removed in "
2212 "Git 3.0"), comment_key_name(config->last_key_id));
2213 + add_comment_char_advice(repo, config);
2214 }
2215 #endif /* WITH_BREAKING_CHANGES */
2216
@@ -2039,7 +2219,8 @@ static void check_deprecated_config(struct repo_config *config)
2219 if (!config->repo->check_deprecated_config)
2220 return;
2221
2042 - check_auto_comment_char_config(&config->comment_char_config);
2222 + check_auto_comment_char_config(config->repo,
2223 + &config->comment_char_config);
2224 }
2225
2226 static int repo_config_callback(const char *key, const char *value,
@@ -2082,6 +2263,7 @@ static void repo_read_config(struct repository *repo)
2263 */
2264 die(_("unknown error occurred while reading the configuration files"));
2265 check_deprecated_config(&config);
2266 + repo_config_release(&config);
2267 }
2268
2269 static void git_config_check_init(struct repository *repo)
t/t3404-rebase-interactive.sh
+11 -1
@@ -1186,9 +1186,19 @@ test_expect_success !WITH_BREAKING_CHANGES 'rebase -i respects core.commentchar=
1186 test_set_editor "$(pwd)/copy-edit-script.sh" &&
1187 git rebase -i HEAD^ 2>err
1188 ) &&
1189 - sed -n "s/^warning: //p" err >actual &&
1189 + sed -n "s/^hint: *\$//p; s/^hint: //p; s/^warning: //p" err >actual &&
1190 cat >expect <<-EOF &&
1191 Support for ${SQ}core.commentChar=auto${SQ} is deprecated and will be removed in Git 3.0
1192 +
1193 + To use the default comment string (#) please run
1194 +
1195 + git config unset core.commentChar
1196 +
1197 + To set a custom comment string please run
1198 +
1199 + git config set core.commentChar <comment string>
1200 +
1201 + where ${SQ}<comment string>${SQ} is the string you wish to use.
1202 EOF
1203 test_cmp expect actual &&
1204 test -z "$(grep -ve "^#" -e "^\$" -e "^pick" edit-script)"
t/t7502-commit-porcelain.sh
+34 -3
@@ -958,10 +958,31 @@ test_expect_success 'commit --status with custom comment character' '
958
959 test_expect_success !WITH_BREAKING_CHANGES 'switch core.commentchar' '
960 test_commit "#foo" foo &&
961 - GIT_EDITOR=.git/FAKE_EDITOR git -c core.commentChar=auto commit --amend 2>err &&
962 - sed -n "s/^warning: //p" err >actual &&
961 + cat >config-include <<-\EOF &&
962 + [core]
963 + commentString=:
964 + commentString=%
965 + commentChar=auto
966 + EOF
967 + test_when_finished "rm config-include" &&
968 + test_config include.path "$(pwd)/config-include" &&
969 + test_config core.commentChar ! &&
970 + GIT_EDITOR=.git/FAKE_EDITOR git commit --amend 2>err &&
971 + sed -n "s/^hint: *\$//p; s/^hint: //p; s/^warning: //p" err >actual &&
972 cat >expect <<-EOF &&
973 Support for ${SQ}core.commentChar=auto${SQ} is deprecated and will be removed in Git 3.0
974 +
975 + To use the default comment string (#) please run
976 +
977 + git config unset core.commentChar
978 + git config unset --file ~/config-include --all core.commentString
979 + git config unset --file ~/config-include core.commentChar
980 +
981 + To set a custom comment string please run
982 +
983 + git config set --file ~/config-include core.commentChar <comment string>
984 +
985 + where ${SQ}<comment string>${SQ} is the string you wish to use.
986 EOF
987 test_cmp expect actual &&
988 test_grep "^; Changes to be committed:" .git/COMMIT_EDITMSG
@@ -990,9 +1011,19 @@ EOF
1011 test_expect_success WITH_BREAKING_CHANGES 'core.commentChar=auto is rejected' '
1012 test_config core.commentChar auto &&
1013 test_must_fail git rev-parse --git-dir 2>err &&
993 - sed -n "s/^fatal: //p" err >actual &&
1014 + sed -n "s/^hint: *\$//p; s/^hint: //p; s/^fatal: //p" err >actual &&
1015 cat >expect <<-EOF &&
1016 Support for ${SQ}core.commentChar=auto${SQ} has been removed in Git 3.0
1017 +
1018 + To use the default comment string (#) please run
1019 +
1020 + git config unset core.commentChar
1021 +
1022 + To set a custom comment string please run
1023 +
1024 + git config set core.commentChar <comment string>
1025 +
1026 + where ${SQ}<comment string>${SQ} is the string you wish to use.
1027 EOF
1028 test_cmp expect actual
1029 '