Merge branch 'ds/sparse-checkout-requires-per-worktree-config'

"git sparse-checkout" wants to work with per-worktree configuration,
but did not work well in a worktree attached to a bare repository.

* ds/sparse-checkout-requires-per-worktree-config:
  config: make git_configset_get_string_tmp() private
  worktree: copy sparse-checkout patterns and config on add
  sparse-checkout: set worktree-config correctly
  config: add repo_config_set_worktree_gently()
  worktree: create init_worktree_config()
  Documentation: add extensions.worktreeConfig details
This commit is contained in:
Junio C Hamano
2022-02-25 15:47:33 -08:00
13 changed files with 353 additions and 49 deletions

View File

@ -6,3 +6,34 @@ extensions.objectFormat::
Note that this setting should only be set by linkgit:git-init[1] or Note that this setting should only be set by linkgit:git-init[1] or
linkgit:git-clone[1]. Trying to change it after initialization will not linkgit:git-clone[1]. Trying to change it after initialization will not
work and will produce hard-to-diagnose issues. work and will produce hard-to-diagnose issues.
extensions.worktreeConfig::
If enabled, then worktrees will load config settings from the
`$GIT_DIR/config.worktree` file in addition to the
`$GIT_COMMON_DIR/config` file. Note that `$GIT_COMMON_DIR` and
`$GIT_DIR` are the same for the main working tree, while other
working trees have `$GIT_DIR` equal to
`$GIT_COMMON_DIR/worktrees/<id>/`. The settings in the
`config.worktree` file will override settings from any other
config files.
+
When enabling `extensions.worktreeConfig`, you must be careful to move
certain values from the common config file to the main working tree's
`config.worktree` file, if present:
+
* `core.worktree` must be moved from `$GIT_COMMON_DIR/config` to
`$GIT_COMMON_DIR/config.worktree`.
* If `core.bare` is true, then it must be moved from `$GIT_COMMON_DIR/config`
to `$GIT_COMMON_DIR/config.worktree`.
+
It may also be beneficial to adjust the locations of `core.sparseCheckout`
and `core.sparseCheckoutCone` depending on your desire for customizable
sparse-checkout settings for each worktree. By default, the `git
sparse-checkout` builtin enables `extensions.worktreeConfig`, assigns
these config values on a per-worktree basis, and uses the
`$GIT_DIR/info/sparse-checkout` file to specify the sparsity for each
worktree independently. See linkgit:git-sparse-checkout[1] for more
details.
+
For historical reasons, `extensions.worktreeConfig` is respected
regardless of the `core.repositoryFormatVersion` setting.

View File

@ -141,9 +141,13 @@ from all available files.
See also <<FILES>>. See also <<FILES>>.
--worktree:: --worktree::
Similar to `--local` except that `.git/config.worktree` is Similar to `--local` except that `$GIT_DIR/config.worktree` is
read from or written to if `extensions.worktreeConfig` is read from or written to if `extensions.worktreeConfig` is
present. If not it's the same as `--local`. enabled. If not it's the same as `--local`. Note that `$GIT_DIR`
is equal to `$GIT_COMMON_DIR` for the main working tree, but is of
the form `$GIT_DIR/worktrees/<id>/` for other working trees. See
linkgit:git-worktree[1] to learn how to enable
`extensions.worktreeConfig`.
-f <config-file>:: -f <config-file>::
--file <config-file>:: --file <config-file>::

View File

@ -31,13 +31,21 @@ COMMANDS
Describe the patterns in the sparse-checkout file. Describe the patterns in the sparse-checkout file.
'set':: 'set'::
Enable the necessary config settings Enable the necessary sparse-checkout config settings
(extensions.worktreeConfig, core.sparseCheckout, (`core.sparseCheckout`, `core.sparseCheckoutCone`, and
core.sparseCheckoutCone) if they are not already enabled, and `index.sparse`) if they are not already set to the desired values,
write a set of patterns to the sparse-checkout file from the and write a set of patterns to the sparse-checkout file from the
list of arguments following the 'set' subcommand. Update the list of arguments following the 'set' subcommand. Update the
working directory to match the new patterns. working directory to match the new patterns.
+ +
To ensure that adjusting the sparse-checkout settings within a worktree
does not alter the sparse-checkout settings in other worktrees, the 'set'
subcommand will upgrade your repository config to use worktree-specific
config if not already present. The sparsity defined by the arguments to
the 'set' subcommand are stored in the worktree-specific sparse-checkout
file. See linkgit:git-worktree[1] and the documentation of
`extensions.worktreeConfig` in linkgit:git-config[1] for more details.
+
When the `--stdin` option is provided, the patterns are read from When the `--stdin` option is provided, the patterns are read from
standard in as a newline-delimited list instead of from the arguments. standard in as a newline-delimited list instead of from the arguments.
+ +

View File

@ -286,8 +286,8 @@ CONFIGURATION FILE
------------------ ------------------
By default, the repository `config` file is shared across all working By default, the repository `config` file is shared across all working
trees. If the config variables `core.bare` or `core.worktree` are trees. If the config variables `core.bare` or `core.worktree` are
already present in the config file, they will be applied to the main present in the common config file and `extensions.worktreeConfig` is
working trees only. disabled, then they will be applied to the main working tree only.
In order to have configuration specific to working trees, you can turn In order to have configuration specific to working trees, you can turn
on the `worktreeConfig` extension, e.g.: on the `worktreeConfig` extension, e.g.:
@ -307,11 +307,16 @@ them to the `config.worktree` of the main working tree. You may also
take this opportunity to review and move other configuration that you take this opportunity to review and move other configuration that you
do not want to share to all working trees: do not want to share to all working trees:
- `core.worktree` and `core.bare` should never be shared - `core.worktree` should never be shared.
- `core.bare` should not be shared if the value is `core.bare=true`.
- `core.sparseCheckout` is recommended per working tree, unless you - `core.sparseCheckout` is recommended per working tree, unless you
are sure you always use sparse checkout for all working trees. are sure you always use sparse checkout for all working trees.
See the documentation of `extensions.worktreeConfig` in
linkgit:git-config[1] for more details.
DETAILS DETAILS
------- -------
Each linked working tree has a private sub-directory in the repository's Each linked working tree has a private sub-directory in the repository's

View File

@ -15,6 +15,7 @@
#include "wt-status.h" #include "wt-status.h"
#include "quote.h" #include "quote.h"
#include "sparse-index.h" #include "sparse-index.h"
#include "worktree.h"
static const char *empty_base = ""; static const char *empty_base = "";
@ -361,26 +362,23 @@ enum sparse_checkout_mode {
static int set_config(enum sparse_checkout_mode mode) static int set_config(enum sparse_checkout_mode mode)
{ {
const char *config_path; /* Update to use worktree config, if not already. */
if (init_worktree_config(the_repository)) {
if (upgrade_repository_format(1) < 0) error(_("failed to initialize worktree config"));
die(_("unable to upgrade repository format to enable worktreeConfig"));
if (git_config_set_gently("extensions.worktreeConfig", "true")) {
error(_("failed to set extensions.worktreeConfig setting"));
return 1; return 1;
} }
config_path = git_path("config.worktree"); if (repo_config_set_worktree_gently(the_repository,
git_config_set_in_file_gently(config_path, "core.sparseCheckout",
"core.sparseCheckout", mode ? "true" : "false") ||
mode ? "true" : NULL); repo_config_set_worktree_gently(the_repository,
"core.sparseCheckoutCone",
git_config_set_in_file_gently(config_path, mode == MODE_CONE_PATTERNS ?
"core.sparseCheckoutCone", "true" : "false"))
mode == MODE_CONE_PATTERNS ? "true" : NULL); return 1;
if (mode == MODE_NO_PATTERNS) if (mode == MODE_NO_PATTERNS)
set_sparse_index_config(the_repository, 0); return set_sparse_index_config(the_repository, 0);
return 0; return 0;
} }

View File

@ -335,6 +335,69 @@ static int add_worktree(const char *path, const char *refname,
strbuf_addf(&sb, "%s/commondir", sb_repo.buf); strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
write_file(sb.buf, "../.."); write_file(sb.buf, "../..");
/*
* If the current worktree has sparse-checkout enabled, then copy
* the sparse-checkout patterns from the current worktree.
*/
if (core_apply_sparse_checkout) {
char *from_file = git_pathdup("info/sparse-checkout");
char *to_file = xstrfmt("%s/info/sparse-checkout",
sb_repo.buf);
if (file_exists(from_file)) {
if (safe_create_leading_directories(to_file) ||
copy_file(to_file, from_file, 0666))
error(_("failed to copy '%s' to '%s'; sparse-checkout may not work correctly"),
from_file, to_file);
}
free(from_file);
free(to_file);
}
/*
* If we are using worktree config, then copy all current config
* values from the current worktree into the new one, that way the
* new worktree behaves the same as this one.
*/
if (repository_format_worktree_config) {
char *from_file = git_pathdup("config.worktree");
char *to_file = xstrfmt("%s/config.worktree",
sb_repo.buf);
if (file_exists(from_file)) {
struct config_set cs = { { 0 } };
const char *core_worktree;
int bare;
if (safe_create_leading_directories(to_file) ||
copy_file(to_file, from_file, 0666)) {
error(_("failed to copy worktree config from '%s' to '%s'"),
from_file, to_file);
goto worktree_copy_cleanup;
}
git_configset_init(&cs);
git_configset_add_file(&cs, from_file);
if (!git_configset_get_bool(&cs, "core.bare", &bare) &&
bare &&
git_config_set_multivar_in_file_gently(
to_file, "core.bare", NULL, "true", 0))
error(_("failed to unset 'core.bare' in '%s'"), to_file);
if (!git_configset_get_value(&cs, "core.worktree", &core_worktree) &&
git_config_set_in_file_gently(to_file,
"core.worktree", NULL))
error(_("failed to unset 'core.worktree' in '%s'"), to_file);
git_configset_clear(&cs);
}
worktree_copy_cleanup:
free(from_file);
free(to_file);
}
strvec_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf); strvec_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf);
strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path); strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path);
cp.git_cmd = 1; cp.git_cmd = 1;

View File

@ -21,6 +21,7 @@
#include "dir.h" #include "dir.h"
#include "color.h" #include "color.h"
#include "refs.h" #include "refs.h"
#include "worktree.h"
struct config_source { struct config_source {
struct config_source *prev; struct config_source *prev;
@ -2294,8 +2295,8 @@ int git_configset_get_string(struct config_set *cs, const char *key, char **dest
return 1; return 1;
} }
int git_configset_get_string_tmp(struct config_set *cs, const char *key, static int git_configset_get_string_tmp(struct config_set *cs, const char *key,
const char **dest) const char **dest)
{ {
const char *value; const char *value;
if (!git_configset_get_value(cs, key, &value)) { if (!git_configset_get_value(cs, key, &value)) {
@ -3000,6 +3001,20 @@ int git_config_set_gently(const char *key, const char *value)
return git_config_set_multivar_gently(key, value, NULL, 0); return git_config_set_multivar_gently(key, value, NULL, 0);
} }
int repo_config_set_worktree_gently(struct repository *r,
const char *key, const char *value)
{
/* Only use worktree-specific config if it is is already enabled. */
if (repository_format_worktree_config) {
char *file = repo_git_path(r, "config.worktree");
int ret = git_config_set_multivar_in_file_gently(
file, key, value, NULL, 0);
free(file);
return ret;
}
return repo_config_set_multivar_gently(r, key, value, NULL, 0);
}
void git_config_set(const char *key, const char *value) void git_config_set(const char *key, const char *value)
{ {
git_config_set_multivar(key, value, NULL, 0); git_config_set_multivar(key, value, NULL, 0);
@ -3297,14 +3312,28 @@ void git_config_set_multivar_in_file(const char *config_filename,
int git_config_set_multivar_gently(const char *key, const char *value, int git_config_set_multivar_gently(const char *key, const char *value,
const char *value_pattern, unsigned flags) const char *value_pattern, unsigned flags)
{ {
return git_config_set_multivar_in_file_gently(NULL, key, value, value_pattern, return repo_config_set_multivar_gently(the_repository, key, value,
flags); value_pattern, flags);
}
int repo_config_set_multivar_gently(struct repository *r, const char *key,
const char *value,
const char *value_pattern, unsigned flags)
{
char *file = repo_git_path(r, "config");
int res = git_config_set_multivar_in_file_gently(file,
key, value,
value_pattern,
flags);
free(file);
return res;
} }
void git_config_set_multivar(const char *key, const char *value, void git_config_set_multivar(const char *key, const char *value,
const char *value_pattern, unsigned flags) const char *value_pattern, unsigned flags)
{ {
git_config_set_multivar_in_file(NULL, key, value, value_pattern, git_config_set_multivar_in_file(git_path("config"),
key, value, value_pattern,
flags); flags);
} }

View File

@ -266,6 +266,13 @@ void git_config_set_in_file(const char *, const char *, const char *);
int git_config_set_gently(const char *, const char *); int git_config_set_gently(const char *, const char *);
/**
* Write a config value that should apply to the current worktree. If
* extensions.worktreeConfig is enabled, then the write will happen in the
* current worktree's config. Otherwise, write to the common config file.
*/
int repo_config_set_worktree_gently(struct repository *, const char *, const char *);
/** /**
* write config values to `.git/config`, takes a key/value pair as parameter. * write config values to `.git/config`, takes a key/value pair as parameter.
*/ */
@ -294,6 +301,7 @@ int git_config_parse_key(const char *, char **, size_t *);
int git_config_set_multivar_gently(const char *, const char *, const char *, unsigned); int git_config_set_multivar_gently(const char *, const char *, const char *, unsigned);
void git_config_set_multivar(const char *, const char *, const char *, unsigned); void git_config_set_multivar(const char *, const char *, const char *, unsigned);
int repo_config_set_multivar_gently(struct repository *, const char *, const char *, const char *, unsigned);
int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, unsigned); int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, unsigned);
/** /**
@ -466,7 +474,6 @@ void git_configset_clear(struct config_set *cs);
int git_configset_get_value(struct config_set *cs, const char *key, const char **dest); int git_configset_get_value(struct config_set *cs, const char *key, const char **dest);
int git_configset_get_string(struct config_set *cs, const char *key, char **dest); int git_configset_get_string(struct config_set *cs, const char *key, char **dest);
int git_configset_get_string_tmp(struct config_set *cs, const char *key, const char **dest);
int git_configset_get_int(struct config_set *cs, const char *key, int *dest); int git_configset_get_int(struct config_set *cs, const char *key, int *dest);
int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest); int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest);
int git_configset_get_bool(struct config_set *cs, const char *key, int *dest); int git_configset_get_bool(struct config_set *cs, const char *key, int *dest);

View File

@ -99,13 +99,9 @@ static int convert_to_sparse_rec(struct index_state *istate,
int set_sparse_index_config(struct repository *repo, int enable) int set_sparse_index_config(struct repository *repo, int enable)
{ {
int res; int res = repo_config_set_worktree_gently(repo,
char *config_path = repo_git_path(repo, "config.worktree"); "index.sparse",
res = git_config_set_in_file_gently(config_path, enable ? "true" : "false");
"index.sparse",
enable ? "true" : NULL);
free(config_path);
prepare_repo_settings(repo); prepare_repo_settings(repo);
repo->settings.sparse_index = enable; repo->settings.sparse_index = enable;
return res; return res;

View File

@ -126,7 +126,7 @@ test_expect_success 'switching to cone mode with non-cone mode patterns' '
cd bad-patterns && cd bad-patterns &&
git sparse-checkout init && git sparse-checkout init &&
git sparse-checkout add dir && git sparse-checkout add dir &&
git config core.sparseCheckoutCone true && git config --worktree core.sparseCheckoutCone true &&
test_must_fail git sparse-checkout add dir 2>err && test_must_fail git sparse-checkout add dir 2>err &&
grep "existing sparse-checkout patterns do not use cone mode" err grep "existing sparse-checkout patterns do not use cone mode" err
) )
@ -155,9 +155,9 @@ test_expect_success 'interaction with clone --no-checkout (unborn index)' '
' '
test_expect_success 'set enables config' ' test_expect_success 'set enables config' '
git init empty-config && git init worktree-config &&
( (
cd empty-config && cd worktree-config &&
test_commit test file && test_commit test file &&
test_path_is_missing .git/config.worktree && test_path_is_missing .git/config.worktree &&
git sparse-checkout set nothing && git sparse-checkout set nothing &&
@ -210,6 +210,21 @@ test_expect_success 'add to sparse-checkout' '
check_files repo "a folder1 folder2" check_files repo "a folder1 folder2"
' '
test_expect_success 'worktree: add copies sparse-checkout patterns' '
cat repo/.git/info/sparse-checkout >old &&
test_when_finished cp old repo/.git/info/sparse-checkout &&
test_when_finished git -C repo worktree remove ../worktree &&
git -C repo sparse-checkout set --no-cone "/*" &&
git -C repo worktree add --quiet ../worktree 2>err &&
test_must_be_empty err &&
new="$(git -C worktree rev-parse --git-path info/sparse-checkout)" &&
test_path_is_file "$new" &&
test_cmp repo/.git/info/sparse-checkout "$new" &&
git -C worktree sparse-checkout set --cone &&
test_cmp_config -C worktree true core.sparseCheckoutCone &&
test_must_fail git -C repo core.sparseCheckoutCone
'
test_expect_success 'cone mode: match patterns' ' test_expect_success 'cone mode: match patterns' '
git -C repo config --worktree core.sparseCheckoutCone true && git -C repo config --worktree core.sparseCheckoutCone true &&
rm -rf repo/a repo/folder1 repo/folder2 && rm -rf repo/a repo/folder1 repo/folder2 &&
@ -261,7 +276,7 @@ test_expect_success 'sparse-index enabled and disabled' '
test_cmp expect actual && test_cmp expect actual &&
git -C repo config --list >config && git -C repo config --list >config &&
! grep index.sparse config test_cmp_config -C repo false index.sparse
' '
test_expect_success 'cone mode: init and set' ' test_expect_success 'cone mode: init and set' '
@ -524,13 +539,13 @@ test_expect_success 'interaction with submodules' '
' '
test_expect_success 'different sparse-checkouts with worktrees' ' test_expect_success 'different sparse-checkouts with worktrees' '
git -C repo sparse-checkout set --cone deep folder1 &&
git -C repo worktree add --detach ../worktree && git -C repo worktree add --detach ../worktree &&
check_files worktree "a deep folder1 folder2" && check_files worktree "a deep folder1" &&
git -C worktree sparse-checkout init --cone && git -C repo sparse-checkout set --cone folder1 &&
git -C repo sparse-checkout set folder1 && git -C worktree sparse-checkout set --cone deep/deeper1 &&
git -C worktree sparse-checkout set deep/deeper1 && check_files repo "a folder1" &&
check_files repo a folder1 && check_files worktree "a deep"
check_files worktree a deep
' '
test_expect_success 'set using filename keeps file on-disk' ' test_expect_success 'set using filename keeps file on-disk' '

View File

@ -165,8 +165,62 @@ test_expect_success '"add" default branch of a bare repo' '
( (
git clone --bare . bare2 && git clone --bare . bare2 &&
cd bare2 && cd bare2 &&
git worktree add ../there3 main git worktree add ../there3 main &&
) cd ../there3 &&
# Simple check that a Git command does not
# immediately fail with the current setup
git status
) &&
cat >expect <<-EOF &&
init.t
EOF
ls there3 >actual &&
test_cmp expect actual
'
test_expect_success '"add" to bare repo with worktree config' '
(
git clone --bare . bare3 &&
cd bare3 &&
git config extensions.worktreeconfig true &&
# Add config values that are erroneous to have in
# a config.worktree file outside of the main
# working tree, to check that Git filters them out
# when copying config during "git worktree add".
git config --worktree core.bare true &&
git config --worktree core.worktree "$(pwd)" &&
# We want to check that bogus.key is copied
git config --worktree bogus.key value &&
git config --unset core.bare &&
git worktree add ../there4 main &&
cd ../there4 &&
# Simple check that a Git command does not
# immediately fail with the current setup
git status &&
git worktree add --detach ../there5 &&
cd ../there5 &&
git status
) &&
# the worktree has the arbitrary value copied.
test_cmp_config -C there4 value bogus.key &&
test_cmp_config -C there5 value bogus.key &&
# however, core.bare and core.worktree were removed.
test_must_fail git -C there4 config core.bare &&
test_must_fail git -C there4 config core.worktree &&
cat >expect <<-EOF &&
init.t
EOF
ls there4 >actual &&
test_cmp expect actual &&
ls there5 >actual &&
test_cmp expect actual
' '
test_expect_success 'checkout with grafts' ' test_expect_success 'checkout with grafts' '

View File

@ -5,6 +5,7 @@
#include "worktree.h" #include "worktree.h"
#include "dir.h" #include "dir.h"
#include "wt-status.h" #include "wt-status.h"
#include "config.h"
void free_worktrees(struct worktree **worktrees) void free_worktrees(struct worktree **worktrees)
{ {
@ -821,3 +822,75 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
*wtpath = path; *wtpath = path;
return 0; return 0;
} }
static int move_config_setting(const char *key, const char *value,
const char *from_file, const char *to_file)
{
if (git_config_set_in_file_gently(to_file, key, value))
return error(_("unable to set %s in '%s'"), key, to_file);
if (git_config_set_in_file_gently(from_file, key, NULL))
return error(_("unable to unset %s in '%s'"), key, from_file);
return 0;
}
int init_worktree_config(struct repository *r)
{
int res = 0;
int bare = 0;
struct config_set cs = { { 0 } };
const char *core_worktree;
char *common_config_file;
char *main_worktree_file;
/*
* If the extension is already enabled, then we can skip the
* upgrade process.
*/
if (repository_format_worktree_config)
return 0;
if ((res = git_config_set_gently("extensions.worktreeConfig", "true")))
return error(_("failed to set extensions.worktreeConfig setting"));
common_config_file = xstrfmt("%s/config", r->commondir);
main_worktree_file = xstrfmt("%s/config.worktree", r->commondir);
git_configset_init(&cs);
git_configset_add_file(&cs, common_config_file);
/*
* If core.bare is true in the common config file, then we need to
* move it to the main worktree's config file or it will break all
* worktrees. If it is false, then leave it in place because it
* _could_ be negating a global core.bare=true.
*/
if (!git_configset_get_bool(&cs, "core.bare", &bare) && bare) {
if ((res = move_config_setting("core.bare", "true",
common_config_file,
main_worktree_file)))
goto cleanup;
}
/*
* If core.worktree is set, then the main worktree is located
* somewhere different than the parent of the common Git dir.
* Relocate that value to avoid breaking all worktrees with this
* upgrade to worktree config.
*/
if (!git_configset_get_value(&cs, "core.worktree", &core_worktree)) {
if ((res = move_config_setting("core.worktree", core_worktree,
common_config_file,
main_worktree_file)))
goto cleanup;
}
/*
* Ensure that we use worktree config for the remaining lifetime
* of the current process.
*/
repository_format_worktree_config = 1;
cleanup:
git_configset_clear(&cs);
free(common_config_file);
free(main_worktree_file);
return res;
}

View File

@ -183,4 +183,25 @@ void strbuf_worktree_ref(const struct worktree *wt,
struct strbuf *sb, struct strbuf *sb,
const char *refname); const char *refname);
/**
* Enable worktree config for the first time. This will make the following
* adjustments:
*
* 1. Add extensions.worktreeConfig=true in the common config file.
*
* 2. If the common config file has a core.worktree value, then that value
* is moved to the main worktree's config.worktree file.
*
* 3. If the common config file has a core.bare enabled, then that value
* is moved to the main worktree's config.worktree file.
*
* If extensions.worktreeConfig is already true, then this method
* terminates early without any of the above steps. The existing config
* arrangement is assumed to be intentional.
*
* Returns 0 on success. Reports an error message and returns non-zero
* if any of these steps fail.
*/
int init_worktree_config(struct repository *r);
#endif #endif