Merge branch 'ao/submodule-wo-gitmodules-checked-out'

The submodule support has been updated to read from the blob at
HEAD:.gitmodules when the .gitmodules file is missing from the
working tree.

* ao/submodule-wo-gitmodules-checked-out:
  t/helper: add test-submodule-nested-repo-config
  submodule: support reading .gitmodules when it's not in the working tree
  submodule: add a helper to check if it is safe to write to .gitmodules
  t7506: clean up .gitmodules properly before setting up new scenario
  submodule: use the 'submodule--helper config' command
  submodule--helper: add a new 'config' subcommand
  t7411: be nicer to future tests and really clean things up
  t7411: merge tests 5 and 6
  submodule: factor out a config_set_in_gitmodules_file_gently function
  submodule: add a print_config_from_gitmodules() helper
This commit is contained in:
Junio C Hamano 2018-11-13 22:37:22 +09:00
commit abb4824d13
16 changed files with 455 additions and 33 deletions

View File

@ -751,6 +751,7 @@ TEST_BUILTINS_OBJS += test-sigchain.o
TEST_BUILTINS_OBJS += test-strcmp-offset.o
TEST_BUILTINS_OBJS += test-string-list.o
TEST_BUILTINS_OBJS += test-submodule-config.o
TEST_BUILTINS_OBJS += test-submodule-nested-repo-config.o
TEST_BUILTINS_OBJS += test-subprocess.o
TEST_BUILTINS_OBJS += test-urlmatch-normalization.o
TEST_BUILTINS_OBJS += test-wildmatch.o

View File

@ -422,11 +422,23 @@ static int grep_submodule(struct grep_opt *opt, struct repository *superproject,
struct repository submodule;
int hit;
if (!is_submodule_active(superproject, path))
return 0;
/*
* NEEDSWORK: submodules functions need to be protected because they
* access the object store via config_from_gitmodules(): the latter
* uses get_oid() which, for now, relies on the global the_repository
* object.
*/
grep_read_lock();
if (repo_submodule_init(&submodule, superproject, path))
if (!is_submodule_active(superproject, path)) {
grep_read_unlock();
return 0;
}
if (repo_submodule_init(&submodule, superproject, path)) {
grep_read_unlock();
return 0;
}
repo_read_gitmodules(&submodule);
@ -440,7 +452,6 @@ static int grep_submodule(struct grep_opt *opt, struct repository *superproject,
* store is no longer global and instead is a member of the repository
* object.
*/
grep_read_lock();
add_to_alternates_memory(submodule.objects->objectdir);
grep_read_unlock();

View File

@ -2141,6 +2141,45 @@ static int check_name(int argc, const char **argv, const char *prefix)
return 0;
}
static int module_config(int argc, const char **argv, const char *prefix)
{
enum {
CHECK_WRITEABLE = 1
} command = 0;
struct option module_config_options[] = {
OPT_CMDMODE(0, "check-writeable", &command,
N_("check if it is safe to write to the .gitmodules file"),
CHECK_WRITEABLE),
OPT_END()
};
const char *const git_submodule_helper_usage[] = {
N_("git submodule--helper config name [value]"),
N_("git submodule--helper config --check-writeable"),
NULL
};
argc = parse_options(argc, argv, prefix, module_config_options,
git_submodule_helper_usage, PARSE_OPT_KEEP_ARGV0);
if (argc == 1 && command == CHECK_WRITEABLE)
return is_writing_gitmodules_ok() ? 0 : -1;
/* Equivalent to ACTION_GET in builtin/config.c */
if (argc == 2)
return print_config_from_gitmodules(the_repository, argv[1]);
/* Equivalent to ACTION_SET in builtin/config.c */
if (argc == 3) {
if (!is_writing_gitmodules_ok())
die(_("please make sure that the .gitmodules file is in the working tree"));
return config_set_in_gitmodules_file_gently(argv[1], argv[2]);
}
usage_with_options(git_submodule_helper_usage, module_config_options);
}
#define SUPPORT_SUPER_PREFIX (1<<0)
struct cmd_struct {
@ -2170,6 +2209,7 @@ static struct cmd_struct commands[] = {
{"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX},
{"is-active", is_active, 0},
{"check-name", check_name, 0},
{"config", module_config, 0},
};
int cmd_submodule__helper(int argc, const char **argv, const char *prefix)

View File

@ -486,6 +486,8 @@ static inline enum object_type object_type(unsigned int mode)
#define INFOATTRIBUTES_FILE "info/attributes"
#define ATTRIBUTE_MACRO_PREFIX "[attr]"
#define GITMODULES_FILE ".gitmodules"
#define GITMODULES_INDEX ":.gitmodules"
#define GITMODULES_HEAD "HEAD:.gitmodules"
#define GIT_NOTES_REF_ENVIRONMENT "GIT_NOTES_REF"
#define GIT_NOTES_DEFAULT_REF "refs/notes/commits"
#define GIT_NOTES_DISPLAY_REF_ENVIRONMENT "GIT_NOTES_DISPLAY_REF"

View File

@ -72,7 +72,7 @@ get_submodule_config () {
value=$(git config submodule."$name"."$option")
if test -z "$value"
then
value=$(git config -f .gitmodules submodule."$name"."$option")
value=$(git submodule--helper config submodule."$name"."$option")
fi
printf '%s' "${value:-$default}"
}
@ -164,6 +164,11 @@ cmd_add()
shift
done
if ! git submodule--helper config --check-writeable >/dev/null 2>&1
then
die "$(eval_gettext "please make sure that the .gitmodules file is in the working tree")"
fi
if test -n "$reference_path"
then
is_absolute_path "$reference_path" ||
@ -288,11 +293,11 @@ or you are unsure what this means choose another name with the '--name' option."
git add --no-warn-embedded-repo $force "$sm_path" ||
die "$(eval_gettext "Failed to add submodule '\$sm_path'")"
git config -f .gitmodules submodule."$sm_name".path "$sm_path" &&
git config -f .gitmodules submodule."$sm_name".url "$repo" &&
git submodule--helper config submodule."$sm_name".path "$sm_path" &&
git submodule--helper config submodule."$sm_name".url "$repo" &&
if test -n "$branch"
then
git config -f .gitmodules submodule."$sm_name".branch "$branch"
git submodule--helper config submodule."$sm_name".branch "$branch"
fi &&
git add --force .gitmodules ||
die "$(eval_gettext "Failed to register submodule '\$sm_path'")"

View File

@ -1,4 +1,5 @@
#include "cache.h"
#include "dir.h"
#include "repository.h"
#include "config.h"
#include "submodule-config.h"
@ -613,8 +614,34 @@ static void submodule_cache_check_init(struct repository *repo)
static void config_from_gitmodules(config_fn_t fn, struct repository *repo, void *data)
{
if (repo->worktree) {
char *file = repo_worktree_path(repo, GITMODULES_FILE);
git_config_from_file(fn, file, data);
struct git_config_source config_source = { 0 };
const struct config_options opts = { 0 };
struct object_id oid;
char *file;
file = repo_worktree_path(repo, GITMODULES_FILE);
if (file_exists(file)) {
config_source.file = file;
} else if (repo->submodule_prefix) {
/*
* When get_oid and config_with_options, used below,
* become able to work on a specific repository, this
* warning branch can be removed.
*/
warning("nested submodules without %s in the working tree are not supported yet",
GITMODULES_FILE);
goto out;
} else if (get_oid(GITMODULES_INDEX, &oid) >= 0) {
config_source.blob = GITMODULES_INDEX;
} else if (get_oid(GITMODULES_HEAD, &oid) >= 0) {
config_source.blob = GITMODULES_HEAD;
} else {
goto out;
}
config_with_options(fn, data, &config_source, &opts);
out:
free(file);
}
}
@ -692,6 +719,43 @@ void submodule_free(struct repository *r)
submodule_cache_clear(r->submodule_cache);
}
static int config_print_callback(const char *var, const char *value, void *cb_data)
{
char *wanted_key = cb_data;
if (!strcmp(wanted_key, var))
printf("%s\n", value);
return 0;
}
int print_config_from_gitmodules(struct repository *repo, const char *key)
{
int ret;
char *store_key;
ret = git_config_parse_key(key, &store_key, NULL);
if (ret < 0)
return CONFIG_INVALID_KEY;
config_from_gitmodules(config_print_callback, repo, store_key);
free(store_key);
return 0;
}
int config_set_in_gitmodules_file_gently(const char *key, const char *value)
{
int ret;
ret = git_config_set_in_file_gently(GITMODULES_FILE, key, value);
if (ret < 0)
/* Maybe the user already did that, don't error out here */
warning(_("Could not update .gitmodules entry %s"), key);
return ret;
}
struct fetch_config {
int *max_children;
int *recurse_submodules;

View File

@ -48,6 +48,8 @@ const struct submodule *submodule_from_path(struct repository *r,
const struct object_id *commit_or_tree,
const char *path);
void submodule_free(struct repository *r);
int print_config_from_gitmodules(struct repository *repo, const char *key);
int config_set_in_gitmodules_file_gently(const char *key, const char *value);
/*
* Returns 0 if the name is syntactically acceptable as a submodule "name"

View File

@ -51,6 +51,24 @@ int is_gitmodules_unmerged(const struct index_state *istate)
return 0;
}
/*
* Check if the .gitmodules file is safe to write.
*
* Writing to the .gitmodules file requires that the file exists in the
* working tree or, if it doesn't, that a brand new .gitmodules file is going
* to be created (i.e. it's neither in the index nor in the current branch).
*
* It is not safe to write to .gitmodules if it's not in the working tree but
* it is in the index or in the current branch, because writing new values
* (and staging them) would blindly overwrite ALL the old content.
*/
int is_writing_gitmodules_ok(void)
{
struct object_id oid;
return file_exists(GITMODULES_FILE) ||
(get_oid(GITMODULES_INDEX, &oid) < 0 && get_oid(GITMODULES_HEAD, &oid) < 0);
}
/*
* Check if the .gitmodules file has unstaged modifications. This must be
* checked before allowing modifications to the .gitmodules file with the
@ -89,6 +107,7 @@ int update_path_in_gitmodules(const char *oldpath, const char *newpath)
{
struct strbuf entry = STRBUF_INIT;
const struct submodule *submodule;
int ret;
if (!file_exists(GITMODULES_FILE)) /* Do nothing without .gitmodules */
return -1;
@ -104,14 +123,9 @@ int update_path_in_gitmodules(const char *oldpath, const char *newpath)
strbuf_addstr(&entry, "submodule.");
strbuf_addstr(&entry, submodule->name);
strbuf_addstr(&entry, ".path");
if (git_config_set_in_file_gently(GITMODULES_FILE, entry.buf, newpath) < 0) {
/* Maybe the user already did that, don't error out here */
warning(_("Could not update .gitmodules entry %s"), entry.buf);
strbuf_release(&entry);
return -1;
}
ret = config_set_in_gitmodules_file_gently(entry.buf, newpath);
strbuf_release(&entry);
return 0;
return ret;
}
/*

View File

@ -40,6 +40,7 @@ struct submodule_update_strategy {
#define SUBMODULE_UPDATE_STRATEGY_INIT {SM_UPDATE_UNSPECIFIED, NULL}
int is_gitmodules_unmerged(const struct index_state *istate);
int is_writing_gitmodules_ok(void);
int is_staging_gitmodules_ok(struct index_state *istate);
int update_path_in_gitmodules(const char *oldpath, const char *newpath);
int remove_path_from_gitmodules(const char *path);

View File

@ -0,0 +1,30 @@
#include "test-tool.h"
#include "submodule-config.h"
static void die_usage(int argc, const char **argv, const char *msg)
{
fprintf(stderr, "%s\n", msg);
fprintf(stderr, "Usage: %s <submodulepath> <config name>\n", argv[0]);
exit(1);
}
int cmd__submodule_nested_repo_config(int argc, const char **argv)
{
struct repository submodule;
if (argc < 3)
die_usage(argc, argv, "Wrong number of arguments.");
setup_git_directory();
if (repo_submodule_init(&submodule, the_repository, argv[1])) {
die_usage(argc, argv, "Submodule not found.");
}
/* Read the config of _child_ submodules. */
print_config_from_gitmodules(&submodule, argv[2]);
submodule_free(the_repository);
return 0;
}

View File

@ -46,6 +46,7 @@ static struct test_cmd cmds[] = {
{ "strcmp-offset", cmd__strcmp_offset },
{ "string-list", cmd__string_list },
{ "submodule-config", cmd__submodule_config },
{ "submodule-nested-repo-config", cmd__submodule_nested_repo_config },
{ "subprocess", cmd__subprocess },
{ "urlmatch-normalization", cmd__urlmatch_normalization },
{ "wildmatch", cmd__wildmatch },

View File

@ -42,6 +42,7 @@ int cmd__sigchain(int argc, const char **argv);
int cmd__strcmp_offset(int argc, const char **argv);
int cmd__string_list(int argc, const char **argv);
int cmd__submodule_config(int argc, const char **argv);
int cmd__submodule_nested_repo_config(int argc, const char **argv);
int cmd__subprocess(int argc, const char **argv);
int cmd__urlmatch_normalization(int argc, const char **argv);
int cmd__wildmatch(int argc, const char **argv);

View File

@ -82,29 +82,23 @@ Submodule name: 'a' for path 'b'
Submodule name: 'submodule' for path 'submodule'
EOF
test_expect_success 'error in one submodule config lets continue' '
test_expect_success 'error in history of one submodule config lets continue, stderr message contains blob ref' '
ORIG=$(git -C super rev-parse HEAD) &&
test_when_finished "git -C super reset --hard $ORIG" &&
(cd super &&
cp .gitmodules .gitmodules.bak &&
echo " value = \"" >>.gitmodules &&
git add .gitmodules &&
mv .gitmodules.bak .gitmodules &&
git commit -m "add error" &&
test-tool submodule-config \
HEAD b \
HEAD submodule \
>actual &&
test_cmp expect_error actual
)
'
test_expect_success 'error message contains blob reference' '
(cd super &&
sha1=$(git rev-parse HEAD) &&
test-tool submodule-config \
HEAD b \
HEAD submodule \
2>actual_err &&
test_i18ngrep "submodule-blob $sha1:.gitmodules" actual_err >/dev/null
>actual \
2>actual_stderr &&
test_cmp expect_error actual &&
test_i18ngrep "submodule-blob $sha1:.gitmodules" actual_stderr >/dev/null
)
'
@ -123,6 +117,8 @@ test_expect_success 'using different treeishs works' '
'
test_expect_success 'error in history in fetchrecursesubmodule lets continue' '
ORIG=$(git -C super rev-parse HEAD) &&
test_when_finished "git -C super reset --hard $ORIG" &&
(cd super &&
git config -f .gitmodules \
submodule.submodule.fetchrecursesubmodules blabla &&
@ -134,8 +130,123 @@ test_expect_success 'error in history in fetchrecursesubmodule lets continue' '
HEAD b \
HEAD submodule \
>actual &&
test_cmp expect_error actual &&
git reset --hard HEAD^
test_cmp expect_error actual
)
'
test_expect_success 'reading submodules config from the working tree with "submodule--helper config"' '
(cd super &&
echo "../submodule" >expect &&
git submodule--helper config submodule.submodule.url >actual &&
test_cmp expect actual
)
'
test_expect_success 'writing submodules config with "submodule--helper config"' '
(cd super &&
echo "new_url" >expect &&
git submodule--helper config submodule.submodule.url "new_url" &&
git submodule--helper config submodule.submodule.url >actual &&
test_cmp expect actual
)
'
test_expect_success 'overwriting unstaged submodules config with "submodule--helper config"' '
test_when_finished "git -C super checkout .gitmodules" &&
(cd super &&
echo "newer_url" >expect &&
git submodule--helper config submodule.submodule.url "newer_url" &&
git submodule--helper config submodule.submodule.url >actual &&
test_cmp expect actual
)
'
test_expect_success 'writeable .gitmodules when it is in the working tree' '
git -C super submodule--helper config --check-writeable
'
test_expect_success 'writeable .gitmodules when it is nowhere in the repository' '
ORIG=$(git -C super rev-parse HEAD) &&
test_when_finished "git -C super reset --hard $ORIG" &&
(cd super &&
git rm .gitmodules &&
git commit -m "remove .gitmodules from the current branch" &&
git submodule--helper config --check-writeable
)
'
test_expect_success 'non-writeable .gitmodules when it is in the index but not in the working tree' '
test_when_finished "git -C super checkout .gitmodules" &&
(cd super &&
rm -f .gitmodules &&
test_must_fail git submodule--helper config --check-writeable
)
'
test_expect_success 'non-writeable .gitmodules when it is in the current branch but not in the index' '
ORIG=$(git -C super rev-parse HEAD) &&
test_when_finished "git -C super reset --hard $ORIG" &&
(cd super &&
git rm .gitmodules &&
test_must_fail git submodule--helper config --check-writeable
)
'
test_expect_success 'reading submodules config from the index when .gitmodules is not in the working tree' '
ORIG=$(git -C super rev-parse HEAD) &&
test_when_finished "git -C super reset --hard $ORIG" &&
(cd super &&
git submodule--helper config submodule.submodule.url "staged_url" &&
git add .gitmodules &&
rm -f .gitmodules &&
echo "staged_url" >expect &&
git submodule--helper config submodule.submodule.url >actual &&
test_cmp expect actual
)
'
test_expect_success 'reading submodules config from the current branch when .gitmodules is not in the index' '
ORIG=$(git -C super rev-parse HEAD) &&
test_when_finished "git -C super reset --hard $ORIG" &&
(cd super &&
git rm .gitmodules &&
echo "../submodule" >expect &&
git submodule--helper config submodule.submodule.url >actual &&
test_cmp expect actual
)
'
test_expect_success 'reading nested submodules config' '
(cd super &&
git init submodule/nested_submodule &&
echo "a" >submodule/nested_submodule/a &&
git -C submodule/nested_submodule add a &&
git -C submodule/nested_submodule commit -m "add a" &&
git -C submodule submodule add ./nested_submodule &&
git -C submodule add nested_submodule &&
git -C submodule commit -m "added nested_submodule" &&
git add submodule &&
git commit -m "updated submodule" &&
echo "./nested_submodule" >expect &&
test-tool submodule-nested-repo-config \
submodule submodule.nested_submodule.url >actual &&
test_cmp expect actual
)
'
# When this test eventually passes, before turning it into
# test_expect_success, remember to replace the test_i18ngrep below with
# a "test_must_be_empty warning" to be sure that the warning is actually
# removed from the code.
test_expect_failure 'reading nested submodules config when .gitmodules is not in the working tree' '
test_when_finished "git -C super/submodule checkout .gitmodules" &&
(cd super &&
echo "./nested_submodule" >expect &&
rm submodule/.gitmodules &&
test-tool submodule-nested-repo-config \
submodule submodule.nested_submodule.url >actual 2>warning &&
test_i18ngrep "nested submodules without %s in the working tree are not supported yet" warning &&
test_cmp expect actual
)
'

View File

@ -0,0 +1,122 @@
#!/bin/sh
#
# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
#
test_description='Test reading/writing .gitmodules when not in the working tree
This test verifies that, when .gitmodules is in the current branch but is not
in the working tree reading from it still works but writing to it does not.
The test setup uses a sparse checkout, however the same scenario can be set up
also by committing .gitmodules and then just removing it from the filesystem.
'
. ./test-lib.sh
test_expect_success 'sparse checkout setup which hides .gitmodules' '
git init upstream &&
git init submodule &&
(cd submodule &&
echo file >file &&
git add file &&
test_tick &&
git commit -m "Add file"
) &&
(cd upstream &&
git submodule add ../submodule &&
test_tick &&
git commit -m "Add submodule"
) &&
git clone upstream super &&
(cd super &&
cat >.git/info/sparse-checkout <<-\EOF &&
/*
!/.gitmodules
EOF
git config core.sparsecheckout true &&
git read-tree -m -u HEAD &&
test_path_is_missing .gitmodules
)
'
test_expect_success 'reading gitmodules config file when it is not checked out' '
echo "../submodule" >expect &&
git -C super submodule--helper config submodule.submodule.url >actual &&
test_cmp expect actual
'
test_expect_success 'not writing gitmodules config file when it is not checked out' '
test_must_fail git -C super submodule--helper config submodule.submodule.url newurl &&
test_path_is_missing super/.gitmodules
'
test_expect_success 'initialising submodule when the gitmodules config is not checked out' '
test_must_fail git -C super config submodule.submodule.url &&
git -C super submodule init &&
git -C super config submodule.submodule.url >actual &&
echo "$(pwd)/submodule" >expect &&
test_cmp expect actual
'
test_expect_success 'updating submodule when the gitmodules config is not checked out' '
test_path_is_missing super/submodule/file &&
git -C super submodule update &&
test_cmp submodule/file super/submodule/file
'
ORIG_SUBMODULE=$(git -C submodule rev-parse HEAD)
ORIG_UPSTREAM=$(git -C upstream rev-parse HEAD)
ORIG_SUPER=$(git -C super rev-parse HEAD)
test_expect_success 're-updating submodule when the gitmodules config is not checked out' '
test_when_finished "git -C submodule reset --hard $ORIG_SUBMODULE;
git -C upstream reset --hard $ORIG_UPSTREAM;
git -C super reset --hard $ORIG_SUPER;
git -C upstream submodule update --remote;
git -C super pull;
git -C super submodule update --remote" &&
(cd submodule &&
echo file2 >file2 &&
git add file2 &&
test_tick &&
git commit -m "Add file2 to submodule"
) &&
(cd upstream &&
git submodule update --remote &&
git add submodule &&
test_tick &&
git commit -m "Update submodule"
) &&
git -C super pull &&
# The --for-status options reads the gitmodules config
git -C super submodule summary --for-status >actual &&
rev1=$(git -C submodule rev-parse --short HEAD) &&
rev2=$(git -C submodule rev-parse --short HEAD^) &&
cat >expect <<-EOF &&
* submodule ${rev1}...${rev2} (1):
< Add file2 to submodule
EOF
test_cmp expect actual &&
# Test that the update actually succeeds
test_path_is_missing super/submodule/file2 &&
git -C super submodule update &&
test_cmp submodule/file2 super/submodule/file2 &&
git -C super status --short >output &&
test_must_be_empty output
'
test_expect_success 'not adding submodules when the gitmodules config is not checked out' '
git clone submodule new_submodule &&
test_must_fail git -C super submodule add ../new_submodule &&
test_path_is_missing .gitmodules
'
# This test checks that the previous "git submodule add" did not leave the
# repository in a spurious state when it failed.
test_expect_success 'init submodule still works even after the previous add failed' '
git -C super submodule init
'
test_done

View File

@ -325,7 +325,8 @@ test_expect_success 'setup superproject with untracked file in nested submodule'
(
cd super &&
git clean -dfx &&
rm .gitmodules &&
git rm .gitmodules &&
git commit -m "remove .gitmodules" &&
git submodule add -f ./sub1 &&
git submodule add -f ./sub2 &&
git submodule add -f ./sub1 sub3 &&

View File

@ -380,4 +380,20 @@ test_expect_success 'grep --recurse-submodules should pass the pattern type alon
fi
'
# Recursing down into nested submodules which do not have .gitmodules in their
# working tree does not work yet. This is because config_from_gitmodules()
# uses get_oid() and the latter is still not able to get objects from an
# arbitrary repository (the nested submodule, in this case).
test_expect_failure 'grep --recurse-submodules with submodules without .gitmodules in the working tree' '
test_when_finished "git -C submodule checkout .gitmodules" &&
rm submodule/.gitmodules &&
git grep --recurse-submodules -e "(.|.)[\d]" >actual &&
cat >expect <<-\EOF &&
a:(1|2)d(3|4)
submodule/a:(1|2)d(3|4)
submodule/sub/a:(1|2)d(3|4)
EOF
test_cmp expect actual
'
test_done