Skip to content

Commit 70e61e3

Browse files
committed
commit: print advice when core.commentString=auto
Support for this config setting was removed in the last commit so print some advice to help the user update their config settings. The advice message explains that the setting is deprecated and will be removed in future. It also shows the commands that the user needs to run to either unset core.commentChar and core.commentString completely or to change the current setting to a fixed comment string. In order to generate this advice we need to parse the config with a callback that records each file where either of the 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. The hard coding of some filenames in add_config_scope_arg() is unfortunate but as this temporary code that will be removed when Git 3.0 is released I decided it wasn't worth adding functions to get the name of the local and worktree config files. 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/core.commentString. Signed-off-by: Phillip Wood <[email protected]>
1 parent ddf27cf commit 70e61e3

File tree

2 files changed

+217
-1
lines changed

2 files changed

+217
-1
lines changed

builtin/commit.c

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
#include "commit-reach.h"
4242
#include "commit-graph.h"
4343
#include "pretty.h"
44+
#include "quote.h"
45+
#include "strmap.h"
4446
#include "trailer.h"
4547

4648
static const char * const builtin_commit_usage[] = {
@@ -684,12 +686,200 @@ static int author_date_is_interesting(void)
684686
}
685687

686688
#ifndef WITH_BREAKING_CHANGES
689+
struct comment_char_cfg {
690+
unsigned last_key_id;
691+
int auto_set_in_file;
692+
struct strintmap key_flags;
693+
size_t alloc, nr;
694+
struct comment_char_cfg_item {
695+
unsigned key_id;
696+
char *path;
697+
enum config_scope scope;
698+
} *item;
699+
};
700+
701+
#define COMMENT_CHAR_CFG_INIT { .key_flags = STRINTMAP_INIT }
702+
703+
static void comment_char_cfg_release(struct comment_char_cfg *cfg)
704+
{
705+
strintmap_clear(&cfg->key_flags);
706+
for (size_t i = 0; i < cfg->nr; i++)
707+
free(cfg->item[i].path);
708+
free(cfg->item);
709+
}
710+
711+
/* Used to track whether the key occurs more than once in a given file */
712+
#define KEY_SEEN_ONCE 1u
713+
#define KEY_SEEN_TWICE 2u
714+
#define COMMENT_KEY_SHIFT(id) (2 * (id))
715+
#define COMMENT_KEY_MASK(id) (3u << COMMENT_KEY_SHIFT(id))
716+
717+
static void set_comment_key_flags(struct comment_char_cfg *cfg,
718+
const char *path, unsigned id, unsigned value)
719+
{
720+
unsigned old = strintmap_get(&cfg->key_flags, path);
721+
unsigned new = (old & ~COMMENT_KEY_MASK(id)) |
722+
value << COMMENT_KEY_SHIFT(id);
723+
724+
strintmap_set(&cfg->key_flags, path, new);
725+
}
726+
727+
static unsigned get_comment_key_flags(struct comment_char_cfg *cfg,
728+
const char *path, unsigned id)
729+
{
730+
unsigned value = strintmap_get(&cfg->key_flags, path);
731+
732+
return (value & COMMENT_KEY_MASK(id)) >> COMMENT_KEY_SHIFT(id);
733+
}
734+
735+
static const char* comment_key_name(unsigned id)
736+
{
737+
static const char *name[] = {
738+
"core.commentChar", "core.commentString",
739+
};
740+
741+
if (id >= ARRAY_SIZE(name))
742+
BUG("invalid comment key id");
743+
744+
return name[id];
745+
}
746+
747+
static int comment_char_config_cb(const char *key, const char *value,
748+
const struct config_context *ctx, void *data)
749+
{
750+
struct comment_char_cfg *cfg = data;
751+
const struct key_value_info *kvi = ctx->kvi;
752+
unsigned key_id;
753+
754+
if (!strcmp(key, "core.commentchar"))
755+
key_id = 0;
756+
else if (!strcmp(key, "core.commentstring"))
757+
key_id = 1;
758+
else
759+
return 0;
760+
761+
cfg->last_key_id = key_id;
762+
if (!kvi->path) {
763+
return 0;
764+
} else if (get_comment_key_flags(cfg, kvi->path, key_id)) {
765+
set_comment_key_flags(cfg, kvi->path, key_id, KEY_SEEN_TWICE);
766+
} else {
767+
struct comment_char_cfg_item *item;
768+
769+
ALLOC_GROW_BY(cfg->item, cfg->nr, 1, cfg->alloc);
770+
item = &cfg->item[cfg->nr - 1];
771+
item->key_id = key_id;
772+
item->scope = kvi->scope;
773+
item->path = xstrdup(kvi->path);
774+
set_comment_key_flags(cfg, kvi->path, key_id, KEY_SEEN_ONCE);
775+
}
776+
cfg->auto_set_in_file = value && !strcmp(value, "auto");
777+
778+
return 0;
779+
}
780+
781+
static void add_config_scope_arg(struct strbuf *buf,
782+
struct comment_char_cfg_item *item)
783+
{
784+
char *global_config = git_global_config();
785+
char *system_config = git_system_config();
786+
787+
if (item->scope == CONFIG_SCOPE_SYSTEM &&
788+
fspatheq(item->path, system_config)) {
789+
strbuf_addstr(buf, "--system ");
790+
} else if (item->scope == CONFIG_SCOPE_GLOBAL &&
791+
fspatheq(item->path, global_config)) {
792+
strbuf_addstr(buf, "--global ");
793+
} else if (item->scope == CONFIG_SCOPE_LOCAL &&
794+
fspatheq(item->path,
795+
mkpath("%s/config",
796+
repo_get_git_dir(the_repository)))) {
797+
; /* --local is the default */
798+
} else if (item->scope == CONFIG_SCOPE_WORKTREE &&
799+
fspatheq(item->path,
800+
mkpath("%s/config.worktree",
801+
repo_get_common_dir(the_repository)))) {
802+
strbuf_addstr(buf, "--worktree ");
803+
} else {
804+
const char *path = item->path;
805+
const char *home = getenv("HOME");
806+
807+
strbuf_addstr(buf, "--file ");
808+
if (home && skip_prefix(path, home, &path)) {
809+
skip_prefix(path, "/", &path);
810+
strbuf_addstr(buf, "~/");
811+
}
812+
sq_quote_buf_pretty(buf, path);
813+
strbuf_addch(buf, ' ');
814+
}
815+
816+
free(global_config);
817+
free(system_config);
818+
}
819+
820+
static void add_optional_comment_char_advice(struct comment_char_cfg *cfg)
821+
{
822+
struct strbuf buf = STRBUF_INIT;
823+
struct comment_char_cfg_item *item;
824+
/* TRANSLATORS this is a place holder for the value of core.commentString */
825+
const char *placeholder = _("<comment string>");
826+
827+
/*
828+
* If auto is set in the last file that we saw advise the user how to
829+
* update their config.
830+
*/
831+
if (!cfg->auto_set_in_file)
832+
return;
833+
834+
for (size_t i = 0; i < cfg->nr; i++) {
835+
item = &cfg->item[i];
836+
837+
strbuf_addstr(&buf, " git config unset ");
838+
add_config_scope_arg(&buf, item);
839+
if (get_comment_key_flags(cfg, item->path, item->key_id) == KEY_SEEN_TWICE)
840+
strbuf_addstr(&buf, "--all ");
841+
strbuf_addf(&buf, "%s\n", comment_key_name(item->key_id));
842+
}
843+
advise(_("\nTo use the default comment string (#) please run\n\n%s"),
844+
buf.buf);
845+
846+
item = &cfg->item[cfg->nr - 1];
847+
strbuf_reset(&buf);
848+
strbuf_addstr(&buf, " git config set ");
849+
add_config_scope_arg(&buf, item);
850+
strbuf_addf(&buf, "%s %s\n", comment_key_name(item->key_id),
851+
placeholder);
852+
advise(_("\nTo set a custom comment string please run\n\n"
853+
"%s\nwhere '%s' is the string you wish to use.\n"),
854+
buf.buf, placeholder);
855+
strbuf_release(&buf);
856+
}
857+
858+
static void advise_auto_comment_char(void)
859+
{
860+
struct comment_char_cfg cfg = COMMENT_CHAR_CFG_INIT;
861+
struct config_options opts = {
862+
.commondir = repo_get_common_dir(the_repository),
863+
.git_dir = repo_get_git_dir(the_repository),
864+
.respect_includes = 1,
865+
};
866+
867+
config_with_options(comment_char_config_cb, &cfg, NULL, the_repository,
868+
&opts);
869+
advise(_("Support for '%s=auto' is deprecated and will be removed in "
870+
"git 3.0\n"), comment_key_name(cfg.last_key_id));
871+
add_optional_comment_char_advice(&cfg);
872+
comment_char_cfg_release(&cfg);
873+
}
874+
687875
static void adjust_comment_line_char(const struct strbuf *sb)
688876
{
689877
char candidates[] = "#;@!$%^&|:";
690878
char *candidate;
691879
const char *p;
692880

881+
advise_auto_comment_char();
882+
693883
if (!memchr(sb->buf, candidates[0], sb->len)) {
694884
free(comment_line_str_to_free);
695885
comment_line_str = comment_line_str_to_free =

t/t7502-commit-porcelain.sh

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,33 @@ test_expect_success 'commit --status with custom comment character' '
958958

959959
test_expect_success !WITH_BREAKING_CHANGES 'switch core.commentchar' '
960960
test_commit "#foo" foo &&
961-
GIT_EDITOR=.git/FAKE_EDITOR git -c core.commentChar=auto commit --amend &&
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 "/^hint: *\$/s///p; /^hint: /s///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 &&
962988
test_grep "^; Changes to be committed:" .git/COMMIT_EDITMSG
963989
'
964990

0 commit comments

Comments
 (0)