mirror of
https://github.com/git/git.git
synced 2024-12-04 23:44:14 +08:00
9b25a0b52e
It can be useful to split your ~/.gitconfig across multiple files. For example, you might have a "main" file which is used on many machines, but a small set of per-machine tweaks. Or you may want to make some of your config public (e.g., clever aliases) while keeping other data back (e.g., your name or other identifying information). Or you may want to include a number of config options in some subset of your repos without copying and pasting (e.g., you want to reference them from the .git/config of participating repos). This patch introduces an include directive for config files. It looks like: [include] path = /path/to/file This is syntactically backwards-compatible with existing git config parsers (i.e., they will see it as another config entry and ignore it unless you are looking up include.path). The implementation provides a "git_config_include" callback which wraps regular config callbacks. Callers can pass it to git_config_from_file, and it will transparently follow any include directives, passing all of the discovered options to the real callback. Include directives are turned on automatically for "regular" git config parsing. This includes calls to git_config, as well as calls to the "git config" program that do not specify a single file (e.g., using "-f", "--global", etc). They are not turned on in other cases, including: 1. Parsing of other config-like files, like .gitmodules. There isn't a real need, and I'd rather be conservative and avoid unnecessary incompatibility or confusion. 2. Reading single files via "git config". This is for two reasons: a. backwards compatibility with scripts looking at config-like files. b. inspection of a specific file probably means you care about just what's in that file, not a general lookup for "do we have this value anywhere at all". If that is not the case, the caller can always specify "--includes". 3. Writing files via "git config"; we want to treat include.* variables as literal items to be copied (or modified), and not expand them. So "git config --unset-all foo.bar" would operate _only_ on .git/config, not any of its included files (just as it also does not operate on ~/.gitconfig). Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
557 lines
15 KiB
C
557 lines
15 KiB
C
#include "builtin.h"
|
|
#include "cache.h"
|
|
#include "color.h"
|
|
#include "parse-options.h"
|
|
|
|
static const char *const builtin_config_usage[] = {
|
|
"git config [options]",
|
|
NULL
|
|
};
|
|
|
|
static char *key;
|
|
static regex_t *key_regexp;
|
|
static regex_t *regexp;
|
|
static int show_keys;
|
|
static int use_key_regexp;
|
|
static int do_all;
|
|
static int do_not_match;
|
|
static int seen;
|
|
static char delim = '=';
|
|
static char key_delim = ' ';
|
|
static char term = '\n';
|
|
|
|
static int use_global_config, use_system_config, use_local_config;
|
|
static const char *given_config_file;
|
|
static int actions, types;
|
|
static const char *get_color_slot, *get_colorbool_slot;
|
|
static int end_null;
|
|
static int respect_includes = -1;
|
|
|
|
#define ACTION_GET (1<<0)
|
|
#define ACTION_GET_ALL (1<<1)
|
|
#define ACTION_GET_REGEXP (1<<2)
|
|
#define ACTION_REPLACE_ALL (1<<3)
|
|
#define ACTION_ADD (1<<4)
|
|
#define ACTION_UNSET (1<<5)
|
|
#define ACTION_UNSET_ALL (1<<6)
|
|
#define ACTION_RENAME_SECTION (1<<7)
|
|
#define ACTION_REMOVE_SECTION (1<<8)
|
|
#define ACTION_LIST (1<<9)
|
|
#define ACTION_EDIT (1<<10)
|
|
#define ACTION_SET (1<<11)
|
|
#define ACTION_SET_ALL (1<<12)
|
|
#define ACTION_GET_COLOR (1<<13)
|
|
#define ACTION_GET_COLORBOOL (1<<14)
|
|
|
|
#define TYPE_BOOL (1<<0)
|
|
#define TYPE_INT (1<<1)
|
|
#define TYPE_BOOL_OR_INT (1<<2)
|
|
#define TYPE_PATH (1<<3)
|
|
|
|
static struct option builtin_config_options[] = {
|
|
OPT_GROUP("Config file location"),
|
|
OPT_BOOLEAN(0, "global", &use_global_config, "use global config file"),
|
|
OPT_BOOLEAN(0, "system", &use_system_config, "use system config file"),
|
|
OPT_BOOLEAN(0, "local", &use_local_config, "use repository config file"),
|
|
OPT_STRING('f', "file", &given_config_file, "file", "use given config file"),
|
|
OPT_GROUP("Action"),
|
|
OPT_BIT(0, "get", &actions, "get value: name [value-regex]", ACTION_GET),
|
|
OPT_BIT(0, "get-all", &actions, "get all values: key [value-regex]", ACTION_GET_ALL),
|
|
OPT_BIT(0, "get-regexp", &actions, "get values for regexp: name-regex [value-regex]", ACTION_GET_REGEXP),
|
|
OPT_BIT(0, "replace-all", &actions, "replace all matching variables: name value [value_regex]", ACTION_REPLACE_ALL),
|
|
OPT_BIT(0, "add", &actions, "adds a new variable: name value", ACTION_ADD),
|
|
OPT_BIT(0, "unset", &actions, "removes a variable: name [value-regex]", ACTION_UNSET),
|
|
OPT_BIT(0, "unset-all", &actions, "removes all matches: name [value-regex]", ACTION_UNSET_ALL),
|
|
OPT_BIT(0, "rename-section", &actions, "rename section: old-name new-name", ACTION_RENAME_SECTION),
|
|
OPT_BIT(0, "remove-section", &actions, "remove a section: name", ACTION_REMOVE_SECTION),
|
|
OPT_BIT('l', "list", &actions, "list all", ACTION_LIST),
|
|
OPT_BIT('e', "edit", &actions, "opens an editor", ACTION_EDIT),
|
|
OPT_STRING(0, "get-color", &get_color_slot, "slot", "find the color configured: [default]"),
|
|
OPT_STRING(0, "get-colorbool", &get_colorbool_slot, "slot", "find the color setting: [stdout-is-tty]"),
|
|
OPT_GROUP("Type"),
|
|
OPT_BIT(0, "bool", &types, "value is \"true\" or \"false\"", TYPE_BOOL),
|
|
OPT_BIT(0, "int", &types, "value is decimal number", TYPE_INT),
|
|
OPT_BIT(0, "bool-or-int", &types, "value is --bool or --int", TYPE_BOOL_OR_INT),
|
|
OPT_BIT(0, "path", &types, "value is a path (file or directory name)", TYPE_PATH),
|
|
OPT_GROUP("Other"),
|
|
OPT_BOOLEAN('z', "null", &end_null, "terminate values with NUL byte"),
|
|
OPT_BOOL(0, "includes", &respect_includes, "respect include directives on lookup"),
|
|
OPT_END(),
|
|
};
|
|
|
|
static void check_argc(int argc, int min, int max) {
|
|
if (argc >= min && argc <= max)
|
|
return;
|
|
error("wrong number of arguments");
|
|
usage_with_options(builtin_config_usage, builtin_config_options);
|
|
}
|
|
|
|
static int show_all_config(const char *key_, const char *value_, void *cb)
|
|
{
|
|
if (value_)
|
|
printf("%s%c%s%c", key_, delim, value_, term);
|
|
else
|
|
printf("%s%c", key_, term);
|
|
return 0;
|
|
}
|
|
|
|
static int show_config(const char *key_, const char *value_, void *cb)
|
|
{
|
|
char value[256];
|
|
const char *vptr = value;
|
|
int must_free_vptr = 0;
|
|
int dup_error = 0;
|
|
int must_print_delim = 0;
|
|
|
|
if (!use_key_regexp && strcmp(key_, key))
|
|
return 0;
|
|
if (use_key_regexp && regexec(key_regexp, key_, 0, NULL, 0))
|
|
return 0;
|
|
if (regexp != NULL &&
|
|
(do_not_match ^ !!regexec(regexp, (value_?value_:""), 0, NULL, 0)))
|
|
return 0;
|
|
|
|
if (show_keys) {
|
|
printf("%s", key_);
|
|
must_print_delim = 1;
|
|
}
|
|
if (seen && !do_all)
|
|
dup_error = 1;
|
|
if (types == TYPE_INT)
|
|
sprintf(value, "%d", git_config_int(key_, value_?value_:""));
|
|
else if (types == TYPE_BOOL)
|
|
vptr = git_config_bool(key_, value_) ? "true" : "false";
|
|
else if (types == TYPE_BOOL_OR_INT) {
|
|
int is_bool, v;
|
|
v = git_config_bool_or_int(key_, value_, &is_bool);
|
|
if (is_bool)
|
|
vptr = v ? "true" : "false";
|
|
else
|
|
sprintf(value, "%d", v);
|
|
} else if (types == TYPE_PATH) {
|
|
git_config_pathname(&vptr, key_, value_);
|
|
must_free_vptr = 1;
|
|
} else if (value_) {
|
|
vptr = value_;
|
|
} else {
|
|
/* Just show the key name */
|
|
vptr = "";
|
|
must_print_delim = 0;
|
|
}
|
|
seen++;
|
|
if (dup_error) {
|
|
error("More than one value for the key %s: %s",
|
|
key_, vptr);
|
|
}
|
|
else {
|
|
if (must_print_delim)
|
|
printf("%c", key_delim);
|
|
printf("%s%c", vptr, term);
|
|
}
|
|
if (must_free_vptr)
|
|
/* If vptr must be freed, it's a pointer to a
|
|
* dynamically allocated buffer, it's safe to cast to
|
|
* const.
|
|
*/
|
|
free((char *)vptr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int get_value(const char *key_, const char *regex_)
|
|
{
|
|
int ret = -1;
|
|
char *global = NULL, *repo_config = NULL;
|
|
const char *system_wide = NULL, *local;
|
|
struct config_include_data inc = CONFIG_INCLUDE_INIT;
|
|
config_fn_t fn;
|
|
void *data;
|
|
|
|
local = given_config_file;
|
|
if (!local) {
|
|
const char *home = getenv("HOME");
|
|
local = repo_config = git_pathdup("config");
|
|
if (home)
|
|
global = xstrdup(mkpath("%s/.gitconfig", home));
|
|
if (git_config_system())
|
|
system_wide = git_etc_gitconfig();
|
|
}
|
|
|
|
if (use_key_regexp) {
|
|
char *tl;
|
|
|
|
/*
|
|
* NEEDSWORK: this naive pattern lowercasing obviously does not
|
|
* work for more complex patterns like "^[^.]*Foo.*bar".
|
|
* Perhaps we should deprecate this altogether someday.
|
|
*/
|
|
|
|
key = xstrdup(key_);
|
|
for (tl = key + strlen(key) - 1;
|
|
tl >= key && *tl != '.';
|
|
tl--)
|
|
*tl = tolower(*tl);
|
|
for (tl = key; *tl && *tl != '.'; tl++)
|
|
*tl = tolower(*tl);
|
|
|
|
key_regexp = (regex_t*)xmalloc(sizeof(regex_t));
|
|
if (regcomp(key_regexp, key, REG_EXTENDED)) {
|
|
fprintf(stderr, "Invalid key pattern: %s\n", key_);
|
|
free(key);
|
|
goto free_strings;
|
|
}
|
|
} else {
|
|
if (git_config_parse_key(key_, &key, NULL))
|
|
goto free_strings;
|
|
}
|
|
|
|
if (regex_) {
|
|
if (regex_[0] == '!') {
|
|
do_not_match = 1;
|
|
regex_++;
|
|
}
|
|
|
|
regexp = (regex_t*)xmalloc(sizeof(regex_t));
|
|
if (regcomp(regexp, regex_, REG_EXTENDED)) {
|
|
fprintf(stderr, "Invalid pattern: %s\n", regex_);
|
|
goto free_strings;
|
|
}
|
|
}
|
|
|
|
fn = show_config;
|
|
data = NULL;
|
|
if (respect_includes) {
|
|
inc.fn = fn;
|
|
inc.data = data;
|
|
fn = git_config_include;
|
|
data = &inc;
|
|
}
|
|
|
|
if (do_all && system_wide)
|
|
git_config_from_file(fn, system_wide, data);
|
|
if (do_all && global)
|
|
git_config_from_file(fn, global, data);
|
|
if (do_all)
|
|
git_config_from_file(fn, local, data);
|
|
git_config_from_parameters(fn, data);
|
|
if (!do_all && !seen)
|
|
git_config_from_file(fn, local, data);
|
|
if (!do_all && !seen && global)
|
|
git_config_from_file(fn, global, data);
|
|
if (!do_all && !seen && system_wide)
|
|
git_config_from_file(fn, system_wide, data);
|
|
|
|
free(key);
|
|
if (regexp) {
|
|
regfree(regexp);
|
|
free(regexp);
|
|
}
|
|
|
|
if (do_all)
|
|
ret = !seen;
|
|
else
|
|
ret = (seen == 1) ? 0 : seen > 1 ? 2 : 1;
|
|
|
|
free_strings:
|
|
free(repo_config);
|
|
free(global);
|
|
return ret;
|
|
}
|
|
|
|
static char *normalize_value(const char *key, const char *value)
|
|
{
|
|
char *normalized;
|
|
|
|
if (!value)
|
|
return NULL;
|
|
|
|
if (types == 0 || types == TYPE_PATH)
|
|
/*
|
|
* We don't do normalization for TYPE_PATH here: If
|
|
* the path is like ~/foobar/, we prefer to store
|
|
* "~/foobar/" in the config file, and to expand the ~
|
|
* when retrieving the value.
|
|
*/
|
|
normalized = xstrdup(value);
|
|
else {
|
|
normalized = xmalloc(64);
|
|
if (types == TYPE_INT) {
|
|
int v = git_config_int(key, value);
|
|
sprintf(normalized, "%d", v);
|
|
}
|
|
else if (types == TYPE_BOOL)
|
|
sprintf(normalized, "%s",
|
|
git_config_bool(key, value) ? "true" : "false");
|
|
else if (types == TYPE_BOOL_OR_INT) {
|
|
int is_bool, v;
|
|
v = git_config_bool_or_int(key, value, &is_bool);
|
|
if (!is_bool)
|
|
sprintf(normalized, "%d", v);
|
|
else
|
|
sprintf(normalized, "%s", v ? "true" : "false");
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
static int get_color_found;
|
|
static const char *get_color_slot;
|
|
static const char *get_colorbool_slot;
|
|
static char parsed_color[COLOR_MAXLEN];
|
|
|
|
static int git_get_color_config(const char *var, const char *value, void *cb)
|
|
{
|
|
if (!strcmp(var, get_color_slot)) {
|
|
if (!value)
|
|
config_error_nonbool(var);
|
|
color_parse(value, var, parsed_color);
|
|
get_color_found = 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void get_color(const char *def_color)
|
|
{
|
|
get_color_found = 0;
|
|
parsed_color[0] = '\0';
|
|
git_config_with_options(git_get_color_config, NULL,
|
|
given_config_file, respect_includes);
|
|
|
|
if (!get_color_found && def_color)
|
|
color_parse(def_color, "command line", parsed_color);
|
|
|
|
fputs(parsed_color, stdout);
|
|
}
|
|
|
|
static int get_colorbool_found;
|
|
static int get_diff_color_found;
|
|
static int get_color_ui_found;
|
|
static int git_get_colorbool_config(const char *var, const char *value,
|
|
void *cb)
|
|
{
|
|
if (!strcmp(var, get_colorbool_slot))
|
|
get_colorbool_found = git_config_colorbool(var, value);
|
|
else if (!strcmp(var, "diff.color"))
|
|
get_diff_color_found = git_config_colorbool(var, value);
|
|
else if (!strcmp(var, "color.ui"))
|
|
get_color_ui_found = git_config_colorbool(var, value);
|
|
return 0;
|
|
}
|
|
|
|
static int get_colorbool(int print)
|
|
{
|
|
get_colorbool_found = -1;
|
|
get_diff_color_found = -1;
|
|
git_config_with_options(git_get_colorbool_config, NULL,
|
|
given_config_file, respect_includes);
|
|
|
|
if (get_colorbool_found < 0) {
|
|
if (!strcmp(get_colorbool_slot, "color.diff"))
|
|
get_colorbool_found = get_diff_color_found;
|
|
if (get_colorbool_found < 0)
|
|
get_colorbool_found = get_color_ui_found;
|
|
}
|
|
|
|
get_colorbool_found = want_color(get_colorbool_found);
|
|
|
|
if (print) {
|
|
printf("%s\n", get_colorbool_found ? "true" : "false");
|
|
return 0;
|
|
} else
|
|
return get_colorbool_found ? 0 : 1;
|
|
}
|
|
|
|
int cmd_config(int argc, const char **argv, const char *prefix)
|
|
{
|
|
int nongit = !startup_info->have_repository;
|
|
char *value;
|
|
|
|
given_config_file = getenv(CONFIG_ENVIRONMENT);
|
|
|
|
argc = parse_options(argc, argv, prefix, builtin_config_options,
|
|
builtin_config_usage,
|
|
PARSE_OPT_STOP_AT_NON_OPTION);
|
|
|
|
if (use_global_config + use_system_config + use_local_config + !!given_config_file > 1) {
|
|
error("only one config file at a time.");
|
|
usage_with_options(builtin_config_usage, builtin_config_options);
|
|
}
|
|
|
|
if (use_global_config) {
|
|
char *home = getenv("HOME");
|
|
if (home) {
|
|
char *user_config = xstrdup(mkpath("%s/.gitconfig", home));
|
|
given_config_file = user_config;
|
|
} else {
|
|
die("$HOME not set");
|
|
}
|
|
}
|
|
else if (use_system_config)
|
|
given_config_file = git_etc_gitconfig();
|
|
else if (use_local_config)
|
|
given_config_file = git_pathdup("config");
|
|
else if (given_config_file) {
|
|
if (!is_absolute_path(given_config_file) && prefix)
|
|
given_config_file =
|
|
xstrdup(prefix_filename(prefix,
|
|
strlen(prefix),
|
|
given_config_file));
|
|
else
|
|
given_config_file = given_config_file;
|
|
}
|
|
|
|
if (respect_includes == -1)
|
|
respect_includes = !given_config_file;
|
|
|
|
if (end_null) {
|
|
term = '\0';
|
|
delim = '\n';
|
|
key_delim = '\n';
|
|
}
|
|
|
|
if (HAS_MULTI_BITS(types)) {
|
|
error("only one type at a time.");
|
|
usage_with_options(builtin_config_usage, builtin_config_options);
|
|
}
|
|
|
|
if (get_color_slot)
|
|
actions |= ACTION_GET_COLOR;
|
|
if (get_colorbool_slot)
|
|
actions |= ACTION_GET_COLORBOOL;
|
|
|
|
if ((get_color_slot || get_colorbool_slot) && types) {
|
|
error("--get-color and variable type are incoherent");
|
|
usage_with_options(builtin_config_usage, builtin_config_options);
|
|
}
|
|
|
|
if (HAS_MULTI_BITS(actions)) {
|
|
error("only one action at a time.");
|
|
usage_with_options(builtin_config_usage, builtin_config_options);
|
|
}
|
|
if (actions == 0)
|
|
switch (argc) {
|
|
case 1: actions = ACTION_GET; break;
|
|
case 2: actions = ACTION_SET; break;
|
|
case 3: actions = ACTION_SET_ALL; break;
|
|
default:
|
|
usage_with_options(builtin_config_usage, builtin_config_options);
|
|
}
|
|
|
|
if (actions == ACTION_LIST) {
|
|
check_argc(argc, 0, 0);
|
|
if (git_config_with_options(show_all_config, NULL,
|
|
given_config_file,
|
|
respect_includes) < 0) {
|
|
if (given_config_file)
|
|
die_errno("unable to read config file '%s'",
|
|
given_config_file);
|
|
else
|
|
die("error processing config file(s)");
|
|
}
|
|
}
|
|
else if (actions == ACTION_EDIT) {
|
|
check_argc(argc, 0, 0);
|
|
if (!given_config_file && nongit)
|
|
die("not in a git directory");
|
|
git_config(git_default_config, NULL);
|
|
launch_editor(given_config_file ?
|
|
given_config_file : git_path("config"),
|
|
NULL, NULL);
|
|
}
|
|
else if (actions == ACTION_SET) {
|
|
int ret;
|
|
check_argc(argc, 2, 2);
|
|
value = normalize_value(argv[0], argv[1]);
|
|
ret = git_config_set_in_file(given_config_file, argv[0], value);
|
|
if (ret == CONFIG_NOTHING_SET)
|
|
error("cannot overwrite multiple values with a single value\n"
|
|
" Use a regexp, --add or --replace-all to change %s.", argv[0]);
|
|
return ret;
|
|
}
|
|
else if (actions == ACTION_SET_ALL) {
|
|
check_argc(argc, 2, 3);
|
|
value = normalize_value(argv[0], argv[1]);
|
|
return git_config_set_multivar_in_file(given_config_file,
|
|
argv[0], value, argv[2], 0);
|
|
}
|
|
else if (actions == ACTION_ADD) {
|
|
check_argc(argc, 2, 2);
|
|
value = normalize_value(argv[0], argv[1]);
|
|
return git_config_set_multivar_in_file(given_config_file,
|
|
argv[0], value, "^$", 0);
|
|
}
|
|
else if (actions == ACTION_REPLACE_ALL) {
|
|
check_argc(argc, 2, 3);
|
|
value = normalize_value(argv[0], argv[1]);
|
|
return git_config_set_multivar_in_file(given_config_file,
|
|
argv[0], value, argv[2], 1);
|
|
}
|
|
else if (actions == ACTION_GET) {
|
|
check_argc(argc, 1, 2);
|
|
return get_value(argv[0], argv[1]);
|
|
}
|
|
else if (actions == ACTION_GET_ALL) {
|
|
do_all = 1;
|
|
check_argc(argc, 1, 2);
|
|
return get_value(argv[0], argv[1]);
|
|
}
|
|
else if (actions == ACTION_GET_REGEXP) {
|
|
show_keys = 1;
|
|
use_key_regexp = 1;
|
|
do_all = 1;
|
|
check_argc(argc, 1, 2);
|
|
return get_value(argv[0], argv[1]);
|
|
}
|
|
else if (actions == ACTION_UNSET) {
|
|
check_argc(argc, 1, 2);
|
|
if (argc == 2)
|
|
return git_config_set_multivar_in_file(given_config_file,
|
|
argv[0], NULL, argv[1], 0);
|
|
else
|
|
return git_config_set_in_file(given_config_file,
|
|
argv[0], NULL);
|
|
}
|
|
else if (actions == ACTION_UNSET_ALL) {
|
|
check_argc(argc, 1, 2);
|
|
return git_config_set_multivar_in_file(given_config_file,
|
|
argv[0], NULL, argv[1], 1);
|
|
}
|
|
else if (actions == ACTION_RENAME_SECTION) {
|
|
int ret;
|
|
check_argc(argc, 2, 2);
|
|
ret = git_config_rename_section_in_file(given_config_file,
|
|
argv[0], argv[1]);
|
|
if (ret < 0)
|
|
return ret;
|
|
if (ret == 0)
|
|
die("No such section!");
|
|
}
|
|
else if (actions == ACTION_REMOVE_SECTION) {
|
|
int ret;
|
|
check_argc(argc, 1, 1);
|
|
ret = git_config_rename_section_in_file(given_config_file,
|
|
argv[0], NULL);
|
|
if (ret < 0)
|
|
return ret;
|
|
if (ret == 0)
|
|
die("No such section!");
|
|
}
|
|
else if (actions == ACTION_GET_COLOR) {
|
|
get_color(argv[0]);
|
|
}
|
|
else if (actions == ACTION_GET_COLORBOOL) {
|
|
if (argc == 1)
|
|
color_stdout_is_tty = git_config_bool("command line", argv[0]);
|
|
return get_colorbool(argc != 0);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int cmd_repo_config(int argc, const char **argv, const char *prefix)
|
|
{
|
|
fprintf(stderr, "WARNING: git repo-config is deprecated in favor of git config.\n");
|
|
return cmd_config(argc, argv, prefix);
|
|
}
|