config: add --comment option to add a comment

Introduce the ability to append comments to modifications made using git-config. Example usage: git config --comment "changed via script" \ --add safe.directory /home/alice/repo.git based on the proposed patch, the output produced is: [safe] directory = /home/alice/repo.git #changed via script Users need to be able to distinguish between config entries made using automation and entries made by a human. Automation can add comments containing a URL pointing to explanations for the change made, avoiding questions from users as to why their config file was changed by a third party. The implementation ensures that a # character is unconditionally prepended to the provided comment string, and that the comment text is appended as a suffix to the changed key-value-pair in the same line of text. Multi-line comments (i.e. comments containing linefeed) are rejected as errors, causing Git to exit without making changes. Comments are aimed at humans who inspect or change their Git config using a pager or editor. Comments are not meant to be read or displayed by git-config at a later time. Signed-off-by: Ralph Seichter <github@seichter.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ralph Seichter committed Mar 12, 2024 at 21:47 UTC 42d5c033945e4fc41d7268bfe4284d37651986b8
12 files changed +78 -45
Documentation/git-config.txt
+8 -3
@@ -9,9 +9,9 @@ git-config - Get and set repository or global options
9 SYNOPSIS
10 --------
11 [verse]
12 -'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
13 -'git config' [<file-option>] [--type=<type>] --add <name> <value>
14 -'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
12 +'git config' [<file-option>] [--type=<type>] [--comment=<value>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
13 +'git config' [<file-option>] [--type=<type>] [--comment=<value>] --add <name> <value>
14 +'git config' [<file-option>] [--type=<type>] [--comment=<value>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
15 'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]
16 'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]
17 'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]
@@ -87,6 +87,11 @@ OPTIONS
87 values. This is the same as providing '^$' as the `value-pattern`
88 in `--replace-all`.
89
90 +--comment <value>::
91 + Append a comment to new or modified lines. A '#' character will be
92 + unconditionally prepended to the value. The value must not contain
93 + linefeed characters (no multi-line comments are permitted).
94 +
95 --get::
96 Get the value for a given key (optionally filtered by a regex
97 matching the value). Returns error code 1 if the key was not
builtin/config.c
+15 -7
@@ -44,6 +44,7 @@ static struct config_options config_options;
44 static int show_origin;
45 static int show_scope;
46 static int fixed_value;
47 +static const char *comment;
48
49 #define ACTION_GET (1<<0)
50 #define ACTION_GET_ALL (1<<1)
@@ -173,6 +174,7 @@ static struct option builtin_config_options[] = {
174 OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
175 OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
176 OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
177 + OPT_STRING(0, "comment", &comment, N_("value"), N_("human-readable comment string (# will be prepended automatically)")),
178 OPT_END(),
179 };
180
@@ -797,6 +799,12 @@ int cmd_config(int argc, const char **argv, const char *prefix)
799 usage_builtin_config();
800 }
801
802 + if (comment &&
803 + !(actions & (ACTION_ADD|ACTION_SET|ACTION_SET_ALL|ACTION_REPLACE_ALL))) {
804 + error(_("--comment is only applicable to add/set/replace operations"));
805 + usage_builtin_config();
806 + }
807 +
808 /* check usage of --fixed-value */
809 if (fixed_value) {
810 int allowed_usage = 0;
@@ -880,7 +888,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
888 check_write();
889 check_argc(argc, 2, 2);
890 value = normalize_value(argv[0], argv[1], &default_kvi);
883 - ret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);
891 + ret = git_config_set_in_file_gently(given_config_source.file, argv[0], comment, value);
892 if (ret == CONFIG_NOTHING_SET)
893 error(_("cannot overwrite multiple values with a single value\n"
894 " Use a regexp, --add or --replace-all to change %s."), argv[0]);
@@ -891,7 +899,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
899 value = normalize_value(argv[0], argv[1], &default_kvi);
900 ret = git_config_set_multivar_in_file_gently(given_config_source.file,
901 argv[0], value, argv[2],
894 - flags);
902 + comment, flags);
903 }
904 else if (actions == ACTION_ADD) {
905 check_write();
@@ -900,7 +908,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
908 ret = git_config_set_multivar_in_file_gently(given_config_source.file,
909 argv[0], value,
910 CONFIG_REGEX_NONE,
903 - flags);
911 + comment, flags);
912 }
913 else if (actions == ACTION_REPLACE_ALL) {
914 check_write();
@@ -908,7 +916,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
916 value = normalize_value(argv[0], argv[1], &default_kvi);
917 ret = git_config_set_multivar_in_file_gently(given_config_source.file,
918 argv[0], value, argv[2],
911 - flags | CONFIG_FLAGS_MULTI_REPLACE);
919 + comment, flags | CONFIG_FLAGS_MULTI_REPLACE);
920 }
921 else if (actions == ACTION_GET) {
922 check_argc(argc, 1, 2);
@@ -936,17 +944,17 @@ int cmd_config(int argc, const char **argv, const char *prefix)
944 if (argc == 2)
945 return git_config_set_multivar_in_file_gently(given_config_source.file,
946 argv[0], NULL, argv[1],
939 - flags);
947 + NULL, flags);
948 else
949 return git_config_set_in_file_gently(given_config_source.file,
942 - argv[0], NULL);
950 + argv[0], NULL, NULL);
951 }
952 else if (actions == ACTION_UNSET_ALL) {
953 check_write();
954 check_argc(argc, 1, 2);
955 return git_config_set_multivar_in_file_gently(given_config_source.file,
956 argv[0], NULL, argv[1],
949 - flags | CONFIG_FLAGS_MULTI_REPLACE);
957 + NULL, flags | CONFIG_FLAGS_MULTI_REPLACE);
958 }
959 else if (actions == ACTION_RENAME_SECTION) {
960 check_write();
builtin/gc.c
+2 -2
@@ -1553,7 +1553,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
1553 die(_("$HOME not set"));
1554 rc = git_config_set_multivar_in_file_gently(
1555 config_file, "maintenance.repo", maintpath,
1556 - CONFIG_REGEX_NONE, 0);
1556 + CONFIG_REGEX_NONE, NULL, 0);
1557 free(global_config_file);
1558
1559 if (rc)
@@ -1620,7 +1620,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
1620 if (!config_file)
1621 die(_("$HOME not set"));
1622 rc = git_config_set_multivar_in_file_gently(
1623 - config_file, key, NULL, maintpath,
1623 + config_file, key, NULL, maintpath, NULL,
1624 CONFIG_FLAGS_MULTI_REPLACE | CONFIG_FLAGS_FIXED_VALUE);
1625 free(global_config_file);
1626
builtin/submodule--helper.c
+1 -1
@@ -1283,7 +1283,7 @@ static void sync_submodule(const char *path, const char *prefix,
1283 submodule_to_gitdir(&sb, path);
1284 strbuf_addstr(&sb, "/config");
1285
1286 - if (git_config_set_in_file_gently(sb.buf, remote_key, sub_origin_url))
1286 + if (git_config_set_in_file_gently(sb.buf, remote_key, NULL, sub_origin_url))
1287 die(_("failed to update remote for submodule '%s'"),
1288 path);
1289
builtin/worktree.c
+2 -2
@@ -365,12 +365,12 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
365 if (!git_configset_get_bool(&cs, "core.bare", &bare) &&
366 bare &&
367 git_config_set_multivar_in_file_gently(
368 - to_file, "core.bare", NULL, "true", 0))
368 + to_file, "core.bare", NULL, "true", NULL, 0))
369 error(_("failed to unset '%s' in '%s'"),
370 "core.bare", to_file);
371 if (!git_configset_get(&cs, "core.worktree") &&
372 git_config_set_in_file_gently(to_file,
373 - "core.worktree", NULL))
373 + "core.worktree", NULL, NULL))
374 error(_("failed to unset '%s' in '%s'"),
375 "core.worktree", to_file);
376
config.c
+17 -8
@@ -3001,6 +3001,7 @@ static ssize_t write_section(int fd, const char *key,
3001 }
3002
3003 static ssize_t write_pair(int fd, const char *key, const char *value,
3004 + const char *comment,
3005 const struct config_store_data *store)
3006 {
3007 int i;
@@ -3041,7 +3042,14 @@ static ssize_t write_pair(int fd, const char *key, const char *value,
3042 strbuf_addch(&sb, value[i]);
3043 break;
3044 }
3044 - strbuf_addf(&sb, "%s\n", quote);
3045 +
3046 + if (comment) {
3047 + if (strchr(comment, '\n'))
3048 + die(_("multi-line comments are not permitted: '%s'"), comment);
3049 + else
3050 + strbuf_addf(&sb, "%s #%s\n", quote, comment);
3051 + } else
3052 + strbuf_addf(&sb, "%s\n", quote);
3053
3054 ret = write_in_full(fd, sb.buf, sb.len);
3055 strbuf_release(&sb);
@@ -3130,9 +3138,9 @@ static void maybe_remove_section(struct config_store_data *store,
3138 }
3139
3140 int git_config_set_in_file_gently(const char *config_filename,
3133 - const char *key, const char *value)
3141 + const char *key, const char *comment, const char *value)
3142 {
3135 - return git_config_set_multivar_in_file_gently(config_filename, key, value, NULL, 0);
3143 + return git_config_set_multivar_in_file_gently(config_filename, key, value, NULL, comment, 0);
3144 }
3145
3146 void git_config_set_in_file(const char *config_filename,
@@ -3153,7 +3161,7 @@ int repo_config_set_worktree_gently(struct repository *r,
3161 if (r->repository_format_worktree_config) {
3162 char *file = repo_git_path(r, "config.worktree");
3163 int ret = git_config_set_multivar_in_file_gently(
3156 - file, key, value, NULL, 0);
3164 + file, key, value, NULL, NULL, 0);
3165 free(file);
3166 return ret;
3167 }
@@ -3195,6 +3203,7 @@ void git_config_set(const char *key, const char *value)
3203 int git_config_set_multivar_in_file_gently(const char *config_filename,
3204 const char *key, const char *value,
3205 const char *value_pattern,
3206 + const char *comment,
3207 unsigned flags)
3208 {
3209 int fd = -1, in_fd = -1;
@@ -3245,7 +3254,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename,
3254 free(store.key);
3255 store.key = xstrdup(key);
3256 if (write_section(fd, key, &store) < 0 ||
3248 - write_pair(fd, key, value, &store) < 0)
3257 + write_pair(fd, key, value, comment, &store) < 0)
3258 goto write_err_out;
3259 } else {
3260 struct stat st;
@@ -3399,7 +3408,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename,
3408 if (write_section(fd, key, &store) < 0)
3409 goto write_err_out;
3410 }
3402 - if (write_pair(fd, key, value, &store) < 0)
3411 + if (write_pair(fd, key, value, comment, &store) < 0)
3412 goto write_err_out;
3413 }
3414
@@ -3444,7 +3453,7 @@ void git_config_set_multivar_in_file(const char *config_filename,
3453 const char *value_pattern, unsigned flags)
3454 {
3455 if (!git_config_set_multivar_in_file_gently(config_filename, key, value,
3447 - value_pattern, flags))
3456 + value_pattern, NULL, flags))
3457 return;
3458 if (value)
3459 die(_("could not set '%s' to '%s'"), key, value);
@@ -3467,7 +3476,7 @@ int repo_config_set_multivar_gently(struct repository *r, const char *key,
3476 int res = git_config_set_multivar_in_file_gently(file,
3477 key, value,
3478 value_pattern,
3470 - flags);
3479 + NULL, flags);
3480 free(file);
3481 return res;
3482 }
config.h
+2 -2
@@ -290,7 +290,7 @@ int git_config_pathname(const char **, const char *, const char *);
290
291 int git_config_expiry_date(timestamp_t *, const char *, const char *);
292 int git_config_color(char *, const char *, const char *);
293 -int git_config_set_in_file_gently(const char *, const char *, const char *);
293 +int git_config_set_in_file_gently(const char *, const char *, const char *, const char *);
294
295 /**
296 * write config values to a specific config file, takes a key/value pair as
@@ -336,7 +336,7 @@ int git_config_parse_key(const char *, char **, size_t *);
336 int git_config_set_multivar_gently(const char *, const char *, const char *, unsigned);
337 void git_config_set_multivar(const char *, const char *, const char *, unsigned);
338 int repo_config_set_multivar_gently(struct repository *, const char *, const char *, const char *, unsigned);
339 -int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, unsigned);
339 +int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, const char *, unsigned);
340
341 /**
342 * takes four parameters:
sequencer.c
+14 -14
@@ -3462,54 +3462,54 @@ static int save_opts(struct replay_opts *opts)
3462
3463 if (opts->no_commit)
3464 res |= git_config_set_in_file_gently(opts_file,
3465 - "options.no-commit", "true");
3465 + "options.no-commit", NULL, "true");
3466 if (opts->edit >= 0)
3467 - res |= git_config_set_in_file_gently(opts_file, "options.edit",
3467 + res |= git_config_set_in_file_gently(opts_file, "options.edit", NULL,
3468 opts->edit ? "true" : "false");
3469 if (opts->allow_empty)
3470 res |= git_config_set_in_file_gently(opts_file,
3471 - "options.allow-empty", "true");
3471 + "options.allow-empty", NULL, "true");
3472 if (opts->allow_empty_message)
3473 res |= git_config_set_in_file_gently(opts_file,
3474 - "options.allow-empty-message", "true");
3474 + "options.allow-empty-message", NULL, "true");
3475 if (opts->keep_redundant_commits)
3476 res |= git_config_set_in_file_gently(opts_file,
3477 - "options.keep-redundant-commits", "true");
3477 + "options.keep-redundant-commits", NULL, "true");
3478 if (opts->signoff)
3479 res |= git_config_set_in_file_gently(opts_file,
3480 - "options.signoff", "true");
3480 + "options.signoff", NULL, "true");
3481 if (opts->record_origin)
3482 res |= git_config_set_in_file_gently(opts_file,
3483 - "options.record-origin", "true");
3483 + "options.record-origin", NULL, "true");
3484 if (opts->allow_ff)
3485 res |= git_config_set_in_file_gently(opts_file,
3486 - "options.allow-ff", "true");
3486 + "options.allow-ff", NULL, "true");
3487 if (opts->mainline) {
3488 struct strbuf buf = STRBUF_INIT;
3489 strbuf_addf(&buf, "%d", opts->mainline);
3490 res |= git_config_set_in_file_gently(opts_file,
3491 - "options.mainline", buf.buf);
3491 + "options.mainline", NULL, buf.buf);
3492 strbuf_release(&buf);
3493 }
3494 if (opts->strategy)
3495 res |= git_config_set_in_file_gently(opts_file,
3496 - "options.strategy", opts->strategy);
3496 + "options.strategy", NULL, opts->strategy);
3497 if (opts->gpg_sign)
3498 res |= git_config_set_in_file_gently(opts_file,
3499 - "options.gpg-sign", opts->gpg_sign);
3499 + "options.gpg-sign", NULL, opts->gpg_sign);
3500 for (size_t i = 0; i < opts->xopts.nr; i++)
3501 res |= git_config_set_multivar_in_file_gently(opts_file,
3502 "options.strategy-option",
3503 - opts->xopts.v[i], "^$", 0);
3503 + opts->xopts.v[i], "^$", NULL, 0);
3504 if (opts->allow_rerere_auto)
3505 res |= git_config_set_in_file_gently(opts_file,
3506 - "options.allow-rerere-auto",
3506 + "options.allow-rerere-auto", NULL,
3507 opts->allow_rerere_auto == RERERE_AUTOUPDATE ?
3508 "true" : "false");
3509
3510 if (opts->explicit_cleanup)
3511 res |= git_config_set_in_file_gently(opts_file,
3512 - "options.default-msg-cleanup",
3512 + "options.default-msg-cleanup", NULL,
3513 describe_cleanup_mode(opts->default_msg_cleanup));
3514 return res;
3515 }
submodule-config.c
+1 -1
@@ -978,7 +978,7 @@ int config_set_in_gitmodules_file_gently(const char *key, const char *value)
978 {
979 int ret;
980
981 - ret = git_config_set_in_file_gently(GITMODULES_FILE, key, value);
981 + ret = git_config_set_in_file_gently(GITMODULES_FILE, key, NULL, value);
982 if (ret < 0)
983 /* Maybe the user already did that, don't error out here */
984 warning(_("Could not update .gitmodules entry %s"), key);
submodule.c
+1 -1
@@ -2046,7 +2046,7 @@ void submodule_unset_core_worktree(const struct submodule *sub)
2046 submodule_name_to_gitdir(&config_path, the_repository, sub->name);
2047 strbuf_addstr(&config_path, "/config");
2048
2049 - if (git_config_set_in_file_gently(config_path.buf, "core.worktree", NULL))
2049 + if (git_config_set_in_file_gently(config_path.buf, "core.worktree", NULL, NULL))
2050 warning(_("Could not unset core.worktree setting in submodule '%s'"),
2051 sub->path);
2052
t/t1300-config.sh
+13 -2
@@ -69,13 +69,24 @@ test_expect_success 'replace with non-match (actually matching)' '
69
70 cat > expect << EOF
71 [section]
72 - penguin = very blue
72 Movie = BadPhysics
73 UPPERCASE = true
75 - penguin = kingpin
74 + penguin = gentoo #Pygoscelis papua
75 + disposition = peckish #find fish
76 + foo = bar ## abc
77 [Sections]
78 WhatEver = Second
79 EOF
80 +test_expect_success 'append comments' '
81 + git config --replace-all --comment="Pygoscelis papua" section.penguin gentoo &&
82 + git config --comment="find fish" section.disposition peckish &&
83 + git config --comment="# abc" section.foo bar &&
84 + test_cmp expect .git/config
85 +'
86 +
87 +test_expect_success 'Prohibited LF in comment' '
88 + test_must_fail git config --comment="a${LF}b" section.k v
89 +'
90
91 test_expect_success 'non-match result' 'test_cmp expect .git/config'
92
worktree.c
+2 -2
@@ -807,9 +807,9 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
807 static int move_config_setting(const char *key, const char *value,
808 const char *from_file, const char *to_file)
809 {
810 - if (git_config_set_in_file_gently(to_file, key, value))
810 + if (git_config_set_in_file_gently(to_file, key, NULL, value))
811 return error(_("unable to set %s in '%s'"), key, to_file);
812 - if (git_config_set_in_file_gently(from_file, key, NULL))
812 + if (git_config_set_in_file_gently(from_file, key, NULL, NULL))
813 return error(_("unable to unset %s in '%s'"), key, from_file);
814 return 0;
815 }