Merge branch 'sb/checkout-recurse-submodules'

"git checkout" is taught the "--recurse-submodules" option.

* sb/checkout-recurse-submodules:
  builtin/read-tree: add --recurse-submodules switch
  builtin/checkout: add --recurse-submodules switch
  entry.c: create submodules when interesting
  unpack-trees: check if we can perform the operation for submodules
  unpack-trees: pass old oid to verify_clean_submodule
  update submodules: add submodule_move_head
  submodule.c: get_super_prefix_or_empty
  update submodules: move up prepare_submodule_repo_env
  submodules: introduce check to see whether to touch a submodule
  update submodules: add a config option to determine if submodules are updated
  update submodules: add submodule config parsing
  make is_submodule_populated gently
  lib-submodule-update.sh: define tests for recursing into submodules
  lib-submodule-update.sh: replace sha1 by hash
  lib-submodule-update: teach test_submodule_content the -C <dir> flag
  lib-submodule-update.sh: do not use ./. as submodule remote
  lib-submodule-update.sh: reorder create_lib_submodule_repo
  submodule--helper.c: remove duplicate code
  connect_work_tree_and_git_dir: safely create leading directories
maint
Junio C Hamano 2017-03-28 14:05:58 -07:00
commit e394fa01d6
17 changed files with 1084 additions and 113 deletions

View File

@ -256,6 +256,13 @@ section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
out anyway. In other words, the ref can be held by more than one
worktree.

--[no-]recurse-submodules::
Using --recurse-submodules will update the content of all initialized
submodules according to the commit recorded in the superproject. If
local modifications in a submodule would be overwritten the checkout
will fail unless `-f` is used. If nothing (or --no-recurse-submodules)
is used, the work trees of submodules will not be updated.

<branch>::
Branch to checkout; if it refers to a branch (i.e., a name that,
when prepended with "refs/heads/", is a valid ref), then that

View File

@ -115,6 +115,12 @@ OPTIONS
directories the index file and index output file are
located in.

--[no-]recurse-submodules::
Using --recurse-submodules will update the content of all initialized
submodules according to the commit recorded in the superproject by
calling read-tree recursively, also setting the submodules HEAD to be
detached at that commit.

--no-sparse-checkout::
Disable sparse checkout support even if `core.sparseCheckout`
is true.

View File

@ -21,12 +21,31 @@
#include "submodule-config.h"
#include "submodule.h"

static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;

static const char * const checkout_usage[] = {
N_("git checkout [<options>] <branch>"),
N_("git checkout [<options>] [<branch>] -- <file>..."),
NULL,
};

static int option_parse_recurse_submodules(const struct option *opt,
const char *arg, int unset)
{
if (unset) {
recurse_submodules = RECURSE_SUBMODULES_OFF;
return 0;
}
if (arg)
recurse_submodules =
parse_update_recurse_submodules_arg(opt->long_name,
arg);
else
recurse_submodules = RECURSE_SUBMODULES_ON;

return 0;
}

struct checkout_opts {
int patch_mode;
int quiet;
@ -1163,6 +1182,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
N_("second guess 'git checkout <no-such-branch>'")),
OPT_BOOL(0, "ignore-other-worktrees", &opts.ignore_other_worktrees,
N_("do not check if another worktree is holding the given ref")),
{ OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules,
"checkout", "control recursive updating of submodules",
PARSE_OPT_OPTARG, option_parse_recurse_submodules },
OPT_BOOL(0, "progress", &opts.show_progress, N_("force progress reporting")),
OPT_END(),
};
@ -1193,6 +1215,12 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
}

if (recurse_submodules != RECURSE_SUBMODULES_OFF) {
git_config(submodule_config, NULL);
if (recurse_submodules != RECURSE_SUBMODULES_DEFAULT)
set_config_update_recurse_submodules(recurse_submodules);
}

if ((!!opts.new_branch + !!opts.new_branch_force + !!opts.new_orphan_branch) > 1)
die(_("-b, -B and --orphan are mutually exclusive"));


View File

@ -618,7 +618,7 @@ static int grep_submodule(struct grep_opt *opt, const unsigned char *sha1,
{
if (!is_submodule_initialized(path))
return 0;
if (!is_submodule_populated(path)) {
if (!is_submodule_populated_gently(path, NULL)) {
/*
* If searching history, check for the presense of the
* submodule's gitdir before skipping the submodule.

View File

@ -15,10 +15,13 @@
#include "builtin.h"
#include "parse-options.h"
#include "resolve-undo.h"
#include "submodule.h"
#include "submodule-config.h"

static int nr_trees;
static int read_empty;
static struct tree *trees[MAX_UNPACK_TREES];
static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;

static int list_tree(unsigned char *sha1)
{
@ -96,6 +99,23 @@ static int debug_merge(const struct cache_entry * const *stages,
return 0;
}

static int option_parse_recurse_submodules(const struct option *opt,
const char *arg, int unset)
{
if (unset) {
recurse_submodules = RECURSE_SUBMODULES_OFF;
return 0;
}
if (arg)
recurse_submodules =
parse_update_recurse_submodules_arg(opt->long_name,
arg);
else
recurse_submodules = RECURSE_SUBMODULES_ON;

return 0;
}

static struct lock_file lock_file;

int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
@ -137,6 +157,9 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
N_("skip applying sparse checkout filter")),
OPT_BOOL(0, "debug-unpack", &opts.debug_unpack,
N_("debug unpack-trees")),
{ OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules,
"checkout", "control recursive updating of submodules",
PARSE_OPT_OPTARG, option_parse_recurse_submodules },
OPT_END()
};

@ -152,6 +175,12 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)

hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR);

if (recurse_submodules != RECURSE_SUBMODULES_DEFAULT) {
gitmodules_config();
git_config(submodule_config, NULL);
set_config_update_recurse_submodules(RECURSE_SUBMODULES_ON);
}

prefix_set = opts.prefix ? 1 : 0;
if (1 < opts.merge + opts.reset + prefix_set)
die("Which one? -m, --reset, or --prefix?");

View File

@ -577,9 +577,7 @@ static int module_clone(int argc, const char **argv, const char *prefix)
const char *name = NULL, *url = NULL, *depth = NULL;
int quiet = 0;
int progress = 0;
FILE *submodule_dot_git;
char *p, *path = NULL, *sm_gitdir;
struct strbuf rel_path = STRBUF_INIT;
struct strbuf sb = STRBUF_INIT;
struct string_list reference = STRING_LIST_INIT_NODUP;
char *sm_alternate = NULL, *error_strategy = NULL;
@ -651,27 +649,12 @@ static int module_clone(int argc, const char **argv, const char *prefix)
strbuf_reset(&sb);
}

/* Write a .git file in the submodule to redirect to the superproject. */
strbuf_addf(&sb, "%s/.git", path);
if (safe_create_leading_directories_const(sb.buf) < 0)
die(_("could not create leading directories of '%s'"), sb.buf);
submodule_dot_git = fopen(sb.buf, "w");
if (!submodule_dot_git)
die_errno(_("cannot open file '%s'"), sb.buf);
/* Connect module worktree and git dir */
connect_work_tree_and_git_dir(path, sm_gitdir);

fprintf_or_die(submodule_dot_git, "gitdir: %s\n",
relative_path(sm_gitdir, path, &rel_path));
if (fclose(submodule_dot_git))
die(_("could not close file %s"), sb.buf);
strbuf_reset(&sb);
strbuf_reset(&rel_path);

/* Redirect the worktree of the submodule in the superproject's config */
p = git_pathdup_submodule(path, "config");
if (!p)
die(_("could not get submodule directory for '%s'"), path);
git_config_set_in_file(p, "core.worktree",
relative_path(path, sm_gitdir, &rel_path));

/* setup alternateLocation and alternateErrorStrategy in the cloned submodule if needed */
git_config_get_string("submodule.alternateLocation", &sm_alternate);
@ -687,7 +670,6 @@ static int module_clone(int argc, const char **argv, const char *prefix)
free(error_strategy);

strbuf_release(&sb);
strbuf_release(&rel_path);
free(sm_gitdir);
free(path);
free(p);

32
dir.c
View File

@ -2765,23 +2765,33 @@ void untracked_cache_add_to_index(struct index_state *istate,
/* Update gitfile and core.worktree setting to connect work tree and git dir */
void connect_work_tree_and_git_dir(const char *work_tree_, const char *git_dir_)
{
struct strbuf file_name = STRBUF_INIT;
struct strbuf gitfile_sb = STRBUF_INIT;
struct strbuf cfg_sb = STRBUF_INIT;
struct strbuf rel_path = STRBUF_INIT;
char *git_dir = real_pathdup(git_dir_, 1);
char *work_tree = real_pathdup(work_tree_, 1);
char *git_dir, *work_tree;

/* Update gitfile */
strbuf_addf(&file_name, "%s/.git", work_tree);
write_file(file_name.buf, "gitdir: %s",
/* Prepare .git file */
strbuf_addf(&gitfile_sb, "%s/.git", work_tree_);
if (safe_create_leading_directories_const(gitfile_sb.buf))
die(_("could not create directories for %s"), gitfile_sb.buf);

/* Prepare config file */
strbuf_addf(&cfg_sb, "%s/config", git_dir_);
if (safe_create_leading_directories_const(cfg_sb.buf))
die(_("could not create directories for %s"), cfg_sb.buf);

git_dir = real_pathdup(git_dir_, 1);
work_tree = real_pathdup(work_tree_, 1);

/* Write .git file */
write_file(gitfile_sb.buf, "gitdir: %s",
relative_path(git_dir, work_tree, &rel_path));

/* Update core.worktree setting */
strbuf_reset(&file_name);
strbuf_addf(&file_name, "%s/config", git_dir);
git_config_set_in_file(file_name.buf, "core.worktree",
git_config_set_in_file(cfg_sb.buf, "core.worktree",
relative_path(work_tree, git_dir, &rel_path));

strbuf_release(&file_name);
strbuf_release(&gitfile_sb);
strbuf_release(&cfg_sb);
strbuf_release(&rel_path);
free(work_tree);
free(git_dir);

30
entry.c
View File

@ -2,6 +2,7 @@
#include "blob.h"
#include "dir.h"
#include "streaming.h"
#include "submodule.h"

static void create_directories(const char *path, int path_len,
const struct checkout *state)
@ -146,6 +147,7 @@ static int write_entry(struct cache_entry *ce,
unsigned long size;
size_t wrote, newsize = 0;
struct stat st;
const struct submodule *sub;

if (ce_mode_s_ifmt == S_IFREG) {
struct stream_filter *filter = get_stream_filter(ce->name,
@ -203,6 +205,10 @@ static int write_entry(struct cache_entry *ce,
return error("cannot create temporary submodule %s", path);
if (mkdir(path, 0777) < 0)
return error("cannot create submodule directory %s", path);
sub = submodule_from_ce(ce);
if (sub)
return submodule_move_head(ce->name,
NULL, oid_to_hex(&ce->oid), SUBMODULE_MOVE_HEAD_FORCE);
break;
default:
return error("unknown file mode for %s in index", path);
@ -259,7 +265,31 @@ int checkout_entry(struct cache_entry *ce,
strbuf_add(&path, ce->name, ce_namelen(ce));

if (!check_path(path.buf, path.len, &st, state->base_dir_len)) {
const struct submodule *sub;
unsigned changed = ce_match_stat(ce, &st, CE_MATCH_IGNORE_VALID|CE_MATCH_IGNORE_SKIP_WORKTREE);
/*
* Needs to be checked before !changed returns early,
* as the possibly empty directory was not changed
*/
sub = submodule_from_ce(ce);
if (sub) {
int err;
if (!is_submodule_populated_gently(ce->name, &err)) {
struct stat sb;
if (lstat(ce->name, &sb))
die(_("could not stat file '%s'"), ce->name);
if (!(st.st_mode & S_IFDIR))
unlink_or_warn(ce->name);

return submodule_move_head(ce->name,
NULL, oid_to_hex(&ce->oid),
SUBMODULE_MOVE_HEAD_FORCE);
} else
return submodule_move_head(ce->name,
"HEAD", oid_to_hex(&ce->oid),
SUBMODULE_MOVE_HEAD_FORCE);
}

if (!changed)
return 0;
if (!state->force) {

View File

@ -234,6 +234,26 @@ int parse_fetch_recurse_submodules_arg(const char *opt, const char *arg)
return parse_fetch_recurse(opt, arg, 1);
}

static int parse_update_recurse(const char *opt, const char *arg,
int die_on_error)
{
switch (git_config_maybe_bool(opt, arg)) {
case 1:
return RECURSE_SUBMODULES_ON;
case 0:
return RECURSE_SUBMODULES_OFF;
default:
if (die_on_error)
die("bad %s argument: %s", opt, arg);
return RECURSE_SUBMODULES_ERROR;
}
}

int parse_update_recurse_submodules_arg(const char *opt, const char *arg)
{
return parse_update_recurse(opt, arg, 1);
}

static int parse_push_recurse(const char *opt, const char *arg,
int die_on_error)
{

View File

@ -22,16 +22,17 @@ struct submodule {
int recommend_shallow;
};

int parse_fetch_recurse_submodules_arg(const char *opt, const char *arg);
int parse_push_recurse_submodules_arg(const char *opt, const char *arg);
int parse_submodule_config_option(const char *var, const char *value);
const struct submodule *submodule_from_name(const unsigned char *commit_or_tree,
const char *name);
const struct submodule *submodule_from_path(const unsigned char *commit_or_tree,
const char *path);
extern int parse_fetch_recurse_submodules_arg(const char *opt, const char *arg);
extern int parse_update_recurse_submodules_arg(const char *opt, const char *arg);
extern int parse_push_recurse_submodules_arg(const char *opt, const char *arg);
extern int parse_submodule_config_option(const char *var, const char *value);
extern const struct submodule *submodule_from_name(
const unsigned char *commit_or_tree, const char *name);
extern const struct submodule *submodule_from_path(
const unsigned char *commit_or_tree, const char *path);
extern int gitmodule_sha1_from_commit(const unsigned char *commit_sha1,
unsigned char *gitmodules_sha1,
struct strbuf *rev);
void submodule_free(void);
extern void submodule_free(void);

#endif /* SUBMODULE_CONFIG_H */

View File

@ -17,6 +17,7 @@
#include "worktree.h"

static int config_fetch_recurse_submodules = RECURSE_SUBMODULES_ON_DEMAND;
static int config_update_recurse_submodules = RECURSE_SUBMODULES_DEFAULT;
static int parallel_jobs = 1;
static struct string_list changed_submodule_paths = STRING_LIST_INIT_NODUP;
static int initialized_fetch_ref_tips;
@ -234,15 +235,12 @@ int is_submodule_initialized(const char *path)
return ret;
}

/*
* Determine if a submodule has been populated at a given 'path'
*/
int is_submodule_populated(const char *path)
int is_submodule_populated_gently(const char *path, int *return_error_code)
{
int ret = 0;
char *gitdir = xstrfmt("%s/.git", path);

if (resolve_gitdir(gitdir))
if (resolve_gitdir_gently(gitdir, return_error_code))
ret = 1;

free(gitdir);
@ -358,6 +356,23 @@ static void print_submodule_summary(struct rev_info *rev, FILE *f,
strbuf_release(&sb);
}

static void prepare_submodule_repo_env_no_git_dir(struct argv_array *out)
{
const char * const *var;

for (var = local_repo_env; *var; var++) {
if (strcmp(*var, CONFIG_DATA_ENVIRONMENT))
argv_array_push(out, *var);
}
}

void prepare_submodule_repo_env(struct argv_array *out)
{
prepare_submodule_repo_env_no_git_dir(out);
argv_array_pushf(out, "%s=%s", GIT_DIR_ENVIRONMENT,
DEFAULT_GIT_DIR_ENVIRONMENT);
}

/* Helper function to display the submodule header line prior to the full
* summary output. If it can locate the submodule objects directory it will
* attempt to lookup both the left and right commits and put them into the
@ -545,6 +560,27 @@ void set_config_fetch_recurse_submodules(int value)
config_fetch_recurse_submodules = value;
}

void set_config_update_recurse_submodules(int value)
{
config_update_recurse_submodules = value;
}

int should_update_submodules(void)
{
return config_update_recurse_submodules == RECURSE_SUBMODULES_ON;
}

const struct submodule *submodule_from_ce(const struct cache_entry *ce)
{
if (!S_ISGITLINK(ce->ce_mode))
return NULL;

if (!should_update_submodules())
return NULL;

return submodule_from_path(null_sha1, ce->name);
}

static int has_remote(const char *refname, const struct object_id *oid,
int flags, void *cb_data)
{
@ -1203,6 +1239,151 @@ out:
return ret;
}

static const char *get_super_prefix_or_empty(void)
{
const char *s = get_super_prefix();
if (!s)
s = "";
return s;
}

static int submodule_has_dirty_index(const struct submodule *sub)
{
struct child_process cp = CHILD_PROCESS_INIT;

prepare_submodule_repo_env_no_git_dir(&cp.env_array);

cp.git_cmd = 1;
argv_array_pushl(&cp.args, "diff-index", "--quiet",
"--cached", "HEAD", NULL);
cp.no_stdin = 1;
cp.no_stdout = 1;
cp.dir = sub->path;
if (start_command(&cp))
die("could not recurse into submodule '%s'", sub->path);

return finish_command(&cp);
}

static void submodule_reset_index(const char *path)
{
struct child_process cp = CHILD_PROCESS_INIT;
prepare_submodule_repo_env_no_git_dir(&cp.env_array);

cp.git_cmd = 1;
cp.no_stdin = 1;
cp.dir = path;

argv_array_pushf(&cp.args, "--super-prefix=%s%s/",
get_super_prefix_or_empty(), path);
argv_array_pushl(&cp.args, "read-tree", "-u", "--reset", NULL);

argv_array_push(&cp.args, EMPTY_TREE_SHA1_HEX);

if (run_command(&cp))
die("could not reset submodule index");
}

/**
* Moves a submodule at a given path from a given head to another new head.
* For edge cases (a submodule coming into existence or removing a submodule)
* pass NULL for old or new respectively.
*/
int submodule_move_head(const char *path,
const char *old,
const char *new,
unsigned flags)
{
int ret = 0;
struct child_process cp = CHILD_PROCESS_INIT;
const struct submodule *sub;

sub = submodule_from_path(null_sha1, path);

if (!sub)
die("BUG: could not get submodule information for '%s'", path);

if (old && !(flags & SUBMODULE_MOVE_HEAD_FORCE)) {
/* Check if the submodule has a dirty index. */
if (submodule_has_dirty_index(sub))
return error(_("submodule '%s' has dirty index"), path);
}

if (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {
if (old) {
if (!submodule_uses_gitfile(path))
absorb_git_dir_into_superproject("", path,
ABSORB_GITDIR_RECURSE_SUBMODULES);
} else {
struct strbuf sb = STRBUF_INIT;
strbuf_addf(&sb, "%s/modules/%s",
get_git_common_dir(), sub->name);
connect_work_tree_and_git_dir(path, sb.buf);
strbuf_release(&sb);

/* make sure the index is clean as well */
submodule_reset_index(path);
}
}

prepare_submodule_repo_env_no_git_dir(&cp.env_array);

cp.git_cmd = 1;
cp.no_stdin = 1;
cp.dir = path;

argv_array_pushf(&cp.args, "--super-prefix=%s%s/",
get_super_prefix_or_empty(), path);
argv_array_pushl(&cp.args, "read-tree", NULL);

if (flags & SUBMODULE_MOVE_HEAD_DRY_RUN)
argv_array_push(&cp.args, "-n");
else
argv_array_push(&cp.args, "-u");

if (flags & SUBMODULE_MOVE_HEAD_FORCE)
argv_array_push(&cp.args, "--reset");
else
argv_array_push(&cp.args, "-m");

argv_array_push(&cp.args, old ? old : EMPTY_TREE_SHA1_HEX);
argv_array_push(&cp.args, new ? new : EMPTY_TREE_SHA1_HEX);

if (run_command(&cp)) {
ret = -1;
goto out;
}

if (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {
if (new) {
struct child_process cp1 = CHILD_PROCESS_INIT;
/* also set the HEAD accordingly */
cp1.git_cmd = 1;
cp1.no_stdin = 1;
cp1.dir = path;

argv_array_pushl(&cp1.args, "update-ref", "HEAD",
new ? new : EMPTY_TREE_SHA1_HEX, NULL);

if (run_command(&cp1)) {
ret = -1;
goto out;
}
} else {
struct strbuf sb = STRBUF_INIT;

strbuf_addf(&sb, "%s/.git", path);
unlink_or_warn(sb.buf);
strbuf_release(&sb);

if (is_empty_dir(path))
rmdir_or_warn(path);
}
}
out:
return ret;
}

static int find_first_merges(struct object_array *result, const char *path,
struct commit *a, struct commit *b)
{
@ -1371,18 +1552,6 @@ int parallel_submodules(void)
return parallel_jobs;
}

void prepare_submodule_repo_env(struct argv_array *out)
{
const char * const *var;

for (var = local_repo_env; *var; var++) {
if (strcmp(*var, CONFIG_DATA_ENVIRONMENT))
argv_array_push(out, *var);
}
argv_array_pushf(out, "%s=%s", GIT_DIR_ENVIRONMENT,
DEFAULT_GIT_DIR_ENVIRONMENT);
}

/*
* Embeds a single submodules git directory into the superprojects git dir,
* non recursively.
@ -1414,11 +1583,8 @@ static void relocate_single_git_dir_into_superproject(const char *prefix,
die(_("could not create directory '%s'"), new_git_dir);
real_new_git_dir = real_pathdup(new_git_dir, 1);

if (!prefix)
prefix = get_super_prefix();

fprintf(stderr, _("Migrating git directory of '%s%s' from\n'%s' to\n'%s'\n"),
prefix ? prefix : "", path,
get_super_prefix_or_empty(), path,
real_old_git_dir, real_new_git_dir);

relocate_gitdir(path, real_old_git_dir, real_new_git_dir);
@ -1445,8 +1611,6 @@ void absorb_git_dir_into_superproject(const char *prefix,

/* Not populated? */
if (!sub_git_dir) {
char *real_new_git_dir;
const char *new_git_dir;
const struct submodule *sub;

if (err_code == READ_GITFILE_ERR_STAT_FAILED) {
@ -1469,13 +1633,8 @@ void absorb_git_dir_into_superproject(const char *prefix,
sub = submodule_from_path(null_sha1, path);
if (!sub)
die(_("could not lookup name for submodule '%s'"), path);
new_git_dir = git_path("modules/%s", sub->name);
if (safe_create_leading_directories_const(new_git_dir) < 0)
die(_("could not create directory '%s'"), new_git_dir);
real_new_git_dir = real_pathdup(new_git_dir, 1);
connect_work_tree_and_git_dir(path, real_new_git_dir);

free(real_new_git_dir);
connect_work_tree_and_git_dir(path,
git_path("modules/%s", sub->name));
} else {
/* Is it already absorbed into the superprojects git dir? */
char *real_sub_git_dir = real_pathdup(sub_git_dir, 1);
@ -1496,8 +1655,7 @@ void absorb_git_dir_into_superproject(const char *prefix,
if (flags & ~ABSORB_GITDIR_RECURSE_SUBMODULES)
die("BUG: we don't know how to pass the flags down?");

if (get_super_prefix())
strbuf_addstr(&sb, get_super_prefix());
strbuf_addstr(&sb, get_super_prefix_or_empty());
strbuf_addstr(&sb, path);
strbuf_addch(&sb, '/');


View File

@ -41,7 +41,13 @@ extern int submodule_config(const char *var, const char *value, void *cb);
extern void gitmodules_config(void);
extern void gitmodules_config_sha1(const unsigned char *commit_sha1);
extern int is_submodule_initialized(const char *path);
extern int is_submodule_populated(const char *path);
/*
* Determine if a submodule has been populated at a given 'path' by checking if
* the <path>/.git resolves to a valid git repository.
* If return_error_code is NULL, die on error.
* Otherwise the return error code is the same as of resolve_gitdir_gently.
*/
extern int is_submodule_populated_gently(const char *path, int *return_error_code);
extern int parse_submodule_update_strategy(const char *value,
struct submodule_update_strategy *dst);
extern const char *submodule_strategy_to_string(const struct submodule_update_strategy *s);
@ -58,6 +64,14 @@ extern void show_submodule_inline_diff(FILE *f, const char *path,
const char *del, const char *add, const char *reset,
const struct diff_options *opt);
extern void set_config_fetch_recurse_submodules(int value);
extern void set_config_update_recurse_submodules(int value);
/* Check if we want to update any submodule.*/
extern int should_update_submodules(void);
/*
* Returns the submodule struct if the given ce entry is a submodule
* and it should be updated. Returns NULL otherwise.
*/
extern const struct submodule *submodule_from_ce(const struct cache_entry *ce);
extern void check_for_new_submodule_commits(unsigned char new_sha1[20]);
extern int fetch_populated_submodules(const struct argv_array *options,
const char *prefix, int command_line_option,
@ -82,6 +96,13 @@ extern int push_unpushed_submodules(struct sha1_array *commits,
extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir);
extern int parallel_submodules(void);

#define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0)
#define SUBMODULE_MOVE_HEAD_FORCE (1<<1)
extern int submodule_move_head(const char *path,
const char *old,
const char *new,
unsigned flags);

/*
* Prepare the "env_array" parameter of a "struct child_process" for executing
* a submodule by clearing any repo-specific envirionment variables, but

View File

@ -4,6 +4,7 @@
# - New submodule (no_submodule => add_sub1)
# - Removed submodule (add_sub1 => remove_sub1)
# - Updated submodule (add_sub1 => modify_sub1)
# - Updated submodule recursively (add_nested_sub => modify_sub1_recursively)
# - Submodule updated to invalid commit (add_sub1 => invalid_sub1)
# - Submodule updated from invalid commit (invalid_sub1 => valid_sub1)
# - Submodule replaced by tracked files in directory (add_sub1 =>
@ -15,23 +16,50 @@
# - Tracked file replaced by submodule (replace_sub1_with_file =>
# replace_file_with_sub1)
#
# --O-----O
# / ^ replace_directory_with_sub1
# / replace_sub1_with_directory
# /----O
# / ^
# / modify_sub1
# O------O-------O
# ^ ^\ ^
# | | \ remove_sub1
# | | -----O-----O
# | | \ ^ replace_file_with_sub1
# | | \ replace_sub1_with_file
# | add_sub1 --O-----O
# no_submodule ^ valid_sub1
# invalid_sub1
# ----O
# / ^
# / remove_sub1
# /
# add_sub1 /-------O---------O--------O modify_sub1_recursively
# | / ^ add_nested_sub
# | / modify_sub1
# v/
# O------O-----------O---------O
# ^ \ ^ replace_directory_with_sub1
# | \ replace_sub1_with_directory
# no_submodule \
# --------O---------O
# \ ^ replace_file_with_sub1
# \ replace_sub1_with_file
# \
# ----O---------O
# ^ valid_sub1
# invalid_sub1
#

create_lib_submodule_repo () {
git init submodule_update_sub1 &&
(
cd submodule_update_sub1 &&
echo "expect" >>.gitignore &&
echo "actual" >>.gitignore &&
echo "x" >file1 &&
echo "y" >file2 &&
git add .gitignore file1 file2 &&
git commit -m "Base inside first submodule" &&
git branch "no_submodule"
) &&
git init submodule_update_sub2 &&
(
cd submodule_update_sub2
echo "expect" >>.gitignore &&
echo "actual" >>.gitignore &&
echo "x" >file1 &&
echo "y" >file2 &&
git add .gitignore file1 file2 &&
git commit -m "nested submodule base" &&
git branch "no_submodule"
) &&
git init submodule_update_repo &&
(
cd submodule_update_repo &&
@ -44,15 +72,16 @@ create_lib_submodule_repo () {
git branch "no_submodule" &&

git checkout -b "add_sub1" &&
git submodule add ./. sub1 &&
git submodule add ../submodule_update_sub1 sub1 &&
git config -f .gitmodules submodule.sub1.ignore all &&
git config submodule.sub1.ignore all &&
git add .gitmodules &&
git commit -m "Add sub1" &&
git checkout -b remove_sub1 &&

git checkout -b remove_sub1 add_sub1 &&
git revert HEAD &&

git checkout -b "modify_sub1" "add_sub1" &&
git checkout -b modify_sub1 add_sub1 &&
git submodule update &&
(
cd sub1 &&
@ -67,7 +96,27 @@ create_lib_submodule_repo () {
git add sub1 &&
git commit -m "Modify sub1" &&

git checkout -b "replace_sub1_with_directory" "add_sub1" &&
git checkout -b add_nested_sub modify_sub1 &&
git -C sub1 checkout -b "add_nested_sub" &&
git -C sub1 submodule add --branch no_submodule ../submodule_update_sub2 sub2 &&
git -C sub1 commit -a -m "add a nested submodule" &&
git add sub1 &&
git commit -a -m "update submodule, that updates a nested submodule" &&
git checkout -b modify_sub1_recursively &&
git -C sub1 checkout -b modify_sub1_recursively &&
git -C sub1/sub2 checkout -b modify_sub1_recursively &&
echo change >sub1/sub2/file3 &&
git -C sub1/sub2 add file3 &&
git -C sub1/sub2 commit -m "make a change in nested sub" &&
git -C sub1 add sub2 &&
git -C sub1 commit -m "update nested sub" &&
git add sub1 &&
git commit -m "update sub1, that updates nested sub" &&
git -C sub1 push origin modify_sub1_recursively &&
git -C sub1/sub2 push origin modify_sub1_recursively &&
git -C sub1 submodule deinit -f --all &&

git checkout -b replace_sub1_with_directory add_sub1 &&
git submodule update &&
git -C sub1 checkout modifications &&
git rm --cached sub1 &&
@ -75,22 +124,25 @@ create_lib_submodule_repo () {
git config -f .gitmodules --remove-section "submodule.sub1" &&
git add .gitmodules sub1/* &&
git commit -m "Replace sub1 with directory" &&

git checkout -b replace_directory_with_sub1 &&
git revert HEAD &&

git checkout -b "replace_sub1_with_file" "add_sub1" &&
git checkout -b replace_sub1_with_file add_sub1 &&
git rm sub1 &&
echo "content" >sub1 &&
git add sub1 &&
git commit -m "Replace sub1 with file" &&

git checkout -b replace_file_with_sub1 &&
git revert HEAD &&

git checkout -b "invalid_sub1" "add_sub1" &&
git checkout -b invalid_sub1 add_sub1 &&
git update-index --cacheinfo 160000 0123456789012345678901234567890123456789 sub1 &&
git commit -m "Invalid sub1 commit" &&
git checkout -b valid_sub1 &&
git revert HEAD &&

git checkout master
)
}
@ -130,6 +182,15 @@ test_git_directory_is_unchanged () {
)
}

test_git_directory_exists() {
test -e ".git/modules/$1" &&
if test -f sub1/.git
then
# does core.worktree point at the right place?
test "$(git -C .git/modules/$1 config core.worktree)" = "../../../$1"
fi
}

# Helper function to be executed at the start of every test below, it sets up
# the submodule repo if it doesn't exist and configures the most problematic
# settings for diff.ignoreSubmodules.
@ -151,15 +212,36 @@ reset_work_tree_to () {
git checkout -f "$1" &&
git status -u -s >actual &&
test_must_be_empty actual &&
sha1=$(git rev-parse --revs-only HEAD:sub1) &&
if test -n "$sha1" &&
test $(cd "sub1" && git rev-parse --verify "$sha1^{commit}")
hash=$(git rev-parse --revs-only HEAD:sub1) &&
if test -n "$hash" &&
test $(cd "../submodule_update_sub1" && git rev-parse --verify "$hash^{commit}")
then
git submodule update --init --recursive "sub1"
fi
)
}

reset_work_tree_to_interested () {
reset_work_tree_to $1 &&
# make the submodule git dirs available
if ! test -d submodule_update/.git/modules/sub1
then
mkdir -p submodule_update/.git/modules &&
cp -r submodule_update_repo/.git/modules/sub1 submodule_update/.git/modules/sub1
GIT_WORK_TREE=. git -C submodule_update/.git/modules/sub1 config --unset core.worktree
fi &&
if ! test -d submodule_update/.git/modules/sub1/modules/sub2
then
mkdir -p submodule_update/.git/modules/sub1/modules &&
cp -r submodule_update_repo/.git/modules/sub1/modules/sub2 submodule_update/.git/modules/sub1/modules/sub2
GIT_WORK_TREE=. git -C submodule_update/.git/modules/sub1/modules/sub2 config --unset core.worktree
fi &&
# indicate we are interested in the submodule:
git -C submodule_update config submodule.sub1.url "bogus" &&
# sub1 might not be checked out, so use the git dir
git -C submodule_update/.git/modules/sub1 config submodule.sub2.url "bogus"
}

# Test that the superproject contains the content according to commit "$1"
# (the work tree must match the index for everything but submodules but the
# index must exactly match the given commit including any submodule SHA-1s).
@ -173,6 +255,11 @@ test_superproject_content () {
# Test that the given submodule at path "$1" contains the content according
# to the submodule commit recorded in the superproject's commit "$2"
test_submodule_content () {
if test x"$1" = "x-C"
then
cd "$2"
shift; shift;
fi
if test $# != 2
then
echo "test_submodule_content needs two arguments"
@ -675,3 +762,464 @@ test_submodule_forced_switch () {
)
'
}

# Test that submodule contents are correctly updated when switching
# between commits that change a submodule.
# Test that the following transitions are correctly handled:
# (These tests are also above in the case where we expect no change
# in the submodule)
# - Updated submodule
# - New submodule
# - Removed submodule
# - Directory containing tracked files replaced by submodule
# - Submodule replaced by tracked files in directory
# - Submodule replaced by tracked file with the same name
# - tracked file replaced by submodule
#
# New test cases
# - Removing a submodule with a git directory absorbs the submodules
# git directory first into the superproject.

test_submodule_switch_recursing () {
command="$1"
RESULTDS=success
if test "$KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS" = 1
then
RESULTDS=failure
fi
RESULTR=success
if test "$KNOWN_FAILURE_SUBMODULE_RECURSIVE_NESTED" = 1
then
RESULTR=failure
fi
RESULTOI=success
if test "$KNOWN_FAILURE_SUBMODULE_OVERWRITE_IGNORED_UNTRACKED" = 1
then
RESULTOI=failure
fi
######################### Appearing submodule #########################
# Switching to a commit letting a submodule appear checks it out ...
test_expect_success "$command: added submodule is checked out" '
prolog &&
reset_work_tree_to_interested no_submodule &&
(
cd submodule_update &&
git branch -t add_sub1 origin/add_sub1 &&
$command add_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'
# ... ignoring an empty existing directory ...
test_expect_success "$command: added submodule is checked out in empty dir" '
prolog &&
reset_work_tree_to_interested no_submodule &&
(
cd submodule_update &&
mkdir sub1 &&
git branch -t add_sub1 origin/add_sub1 &&
$command add_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'
# ... unless there is an untracked file in its place.
test_expect_success "$command: added submodule doesn't remove untracked file with same name" '
prolog &&
reset_work_tree_to_interested no_submodule &&
(
cd submodule_update &&
git branch -t add_sub1 origin/add_sub1 &&
: >sub1 &&
test_must_fail $command add_sub1 &&
test_superproject_content origin/no_submodule &&
test_must_be_empty sub1
)
'
# ... but an ignored file is fine.
test_expect_$RESULTOI "$command: added submodule removes an untracked ignored file" '
test_when_finished "rm submodule_update/.git/info/exclude" &&
prolog &&
reset_work_tree_to_interested no_submodule &&
(
cd submodule_update &&
git branch -t add_sub1 origin/add_sub1 &&
: >sub1 &&
echo sub1 >.git/info/exclude
$command add_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'
# Replacing a tracked file with a submodule produces a checked out submodule
test_expect_success "$command: replace tracked file with submodule checks out submodule" '
prolog &&
reset_work_tree_to_interested replace_sub1_with_file &&
(
cd submodule_update &&
git branch -t replace_file_with_sub1 origin/replace_file_with_sub1 &&
$command replace_file_with_sub1 &&
test_superproject_content origin/replace_file_with_sub1 &&
test_submodule_content sub1 origin/replace_file_with_sub1
)
'
# ... as does removing a directory with tracked files with a submodule.
test_expect_success "$command: replace directory with submodule" '
prolog &&
reset_work_tree_to_interested replace_sub1_with_directory &&
(
cd submodule_update &&
git branch -t replace_directory_with_sub1 origin/replace_directory_with_sub1 &&
$command replace_directory_with_sub1 &&
test_superproject_content origin/replace_directory_with_sub1 &&
test_submodule_content sub1 origin/replace_directory_with_sub1
)
'

######################## Disappearing submodule #######################
# Removing a submodule removes its work tree ...
test_expect_success "$command: removed submodule removes submodules working tree" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t remove_sub1 origin/remove_sub1 &&
$command remove_sub1 &&
test_superproject_content origin/remove_sub1 &&
! test -e sub1
)
'
# ... absorbing a .git directory along the way.
test_expect_success "$command: removed submodule absorbs submodules .git directory" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t remove_sub1 origin/remove_sub1 &&
replace_gitfile_with_git_dir sub1 &&
rm -rf .git/modules &&
$command remove_sub1 &&
test_superproject_content origin/remove_sub1 &&
! test -e sub1 &&
test_git_directory_exists sub1
)
'
# Replacing a submodule with files in a directory must succeeds
# when the submodule is clean
test_expect_$RESULTDS "$command: replace submodule with a directory" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&
$command replace_sub1_with_directory &&
test_superproject_content origin/replace_sub1_with_directory &&
test_submodule_content sub1 origin/replace_sub1_with_directory
)
'
# ... absorbing a .git directory.
test_expect_$RESULTDS "$command: replace submodule containing a .git directory with a directory must absorb the git dir" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&
replace_gitfile_with_git_dir sub1 &&
rm -rf .git/modules &&
$command replace_sub1_with_directory &&
test_superproject_content origin/replace_sub1_with_directory &&
test_git_directory_exists sub1
)
'

# Replacing it with a file ...
test_expect_success "$command: replace submodule with a file" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_file origin/replace_sub1_with_file &&
$command replace_sub1_with_file &&
test_superproject_content origin/replace_sub1_with_file &&
test -f sub1
)
'

# ... must check its local work tree for untracked files
test_expect_$RESULTDS "$command: replace submodule with a file must fail with untracked files" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_file origin/replace_sub1_with_file &&
: >sub1/untrackedfile &&
test_must_fail $command replace_sub1_with_file &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'

# ... and ignored files are ignored
test_expect_success "$command: replace submodule with a file works ignores ignored files in submodule" '
test_when_finished "rm submodule_update/.git/modules/sub1/info/exclude" &&
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_file origin/replace_sub1_with_file &&
: >sub1/ignored &&
$command replace_sub1_with_file &&
test_superproject_content origin/replace_sub1_with_file &&
test -f sub1
)
'

########################## Modified submodule #########################
# Updating a submodule sha1 updates the submodule's work tree
test_expect_success "$command: modified submodule updates submodule work tree" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t modify_sub1 origin/modify_sub1 &&
$command modify_sub1 &&
test_superproject_content origin/modify_sub1 &&
test_submodule_content sub1 origin/modify_sub1
)
'

# Updating a submodule to an invalid sha1 doesn't update the
# superproject nor the submodule's work tree.
test_expect_success "$command: updating to a missing submodule commit fails" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t invalid_sub1 origin/invalid_sub1 &&
test_must_fail $command invalid_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'

# recursing deeper than one level doesn't work yet.
test_expect_$RESULTR "$command: modified submodule updates submodule recursively" '
prolog &&
reset_work_tree_to_interested add_nested_sub &&
(
cd submodule_update &&
git branch -t modify_sub1_recursively origin/modify_sub1_recursively &&
$command modify_sub1_recursively &&
test_superproject_content origin/modify_sub1_recursively &&
test_submodule_content sub1 origin/modify_sub1_recursively &&
test_submodule_content -C sub1 sub2 origin/modify_sub1_recursively
)
'
}

# Test that submodule contents are updated when switching between commits
# that change a submodule, but throwing away local changes in
# the superproject as well as the submodule is allowed.
test_submodule_forced_switch_recursing () {
command="$1"
RESULT=success
if test "$KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS" = 1
then
RESULT=failure
fi
######################### Appearing submodule #########################
# Switching to a commit letting a submodule appear creates empty dir ...
test_expect_success "$command: added submodule is checked out" '
prolog &&
reset_work_tree_to_interested no_submodule &&
(
cd submodule_update &&
git branch -t add_sub1 origin/add_sub1 &&
$command add_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'
# ... and doesn't care if it already exists ...
test_expect_success "$command: added submodule ignores empty directory" '
prolog &&
reset_work_tree_to_interested no_submodule &&
(
cd submodule_update &&
git branch -t add_sub1 origin/add_sub1 &&
mkdir sub1 &&
$command add_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'
# ... not caring about an untracked file either
test_expect_success "$command: added submodule does remove untracked unignored file with same name when forced" '
prolog &&
reset_work_tree_to_interested no_submodule &&
(
cd submodule_update &&
git branch -t add_sub1 origin/add_sub1 &&
>sub1 &&
$command add_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'
# Replacing a tracked file with a submodule checks out the submodule
test_expect_success "$command: replace tracked file with submodule populates the submodule" '
prolog &&
reset_work_tree_to_interested replace_sub1_with_file &&
(
cd submodule_update &&
git branch -t replace_file_with_sub1 origin/replace_file_with_sub1 &&
$command replace_file_with_sub1 &&
test_superproject_content origin/replace_file_with_sub1 &&
test_submodule_content sub1 origin/replace_file_with_sub1
)
'
# ... as does removing a directory with tracked files with a
# submodule.
test_expect_success "$command: replace directory with submodule" '
prolog &&
reset_work_tree_to_interested replace_sub1_with_directory &&
(
cd submodule_update &&
git branch -t replace_directory_with_sub1 origin/replace_directory_with_sub1 &&
$command replace_directory_with_sub1 &&
test_superproject_content origin/replace_directory_with_sub1 &&
test_submodule_content sub1 origin/replace_directory_with_sub1
)
'

######################## Disappearing submodule #######################
# Removing a submodule doesn't remove its work tree ...
test_expect_success "$command: removed submodule leaves submodule directory and its contents in place" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t remove_sub1 origin/remove_sub1 &&
$command remove_sub1 &&
test_superproject_content origin/remove_sub1 &&
! test -e sub1
)
'
# ... especially when it contains a .git directory.
test_expect_success "$command: removed submodule leaves submodule containing a .git directory alone" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t remove_sub1 origin/remove_sub1 &&
replace_gitfile_with_git_dir sub1 &&
rm -rf .git/modules/sub1 &&
$command remove_sub1 &&
test_superproject_content origin/remove_sub1 &&
test_git_directory_exists sub1 &&
! test -e sub1
)
'
# Replacing a submodule with files in a directory ...
test_expect_success "$command: replace submodule with a directory" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&
$command replace_sub1_with_directory &&
test_superproject_content origin/replace_sub1_with_directory
)
'
# ... absorbing a .git directory.
test_expect_success "$command: replace submodule containing a .git directory with a directory must fail" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&
replace_gitfile_with_git_dir sub1 &&
rm -rf .git/modules/sub1 &&
$command replace_sub1_with_directory &&
test_superproject_content origin/replace_sub1_with_directory &&
test_submodule_content sub1 origin/modify_sub1
test_git_directory_exists sub1
)
'
# Replacing it with a file
test_expect_success "$command: replace submodule with a file" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_file origin/replace_sub1_with_file &&
$command replace_sub1_with_file &&
test_superproject_content origin/replace_sub1_with_file
)
'

# ... even if the submodule contains ignored files
test_expect_success "$command: replace submodule with a file ignoring ignored files" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_file origin/replace_sub1_with_file &&
: >sub1/expect &&
$command replace_sub1_with_file &&
test_superproject_content origin/replace_sub1_with_file
)
'

# ... but stops for untracked files that would be lost
test_expect_$RESULT "$command: replace submodule with a file stops for untracked files" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t replace_sub1_with_file origin/replace_sub1_with_file &&
: >sub1/untracked_file &&
test_must_fail $command replace_sub1_with_file &&
test_superproject_content origin/add_sub1 &&
test -f sub1/untracked_file
)
'

########################## Modified submodule #########################
# Updating a submodule sha1 updates the submodule's work tree
test_expect_success "$command: modified submodule updates submodule work tree" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t modify_sub1 origin/modify_sub1 &&
$command modify_sub1 &&
test_superproject_content origin/modify_sub1 &&
test_submodule_content sub1 origin/modify_sub1
)
'
# Updating a submodule to an invalid sha1 doesn't update the
# submodule's work tree, subsequent update will fail
test_expect_success "$command: modified submodule does not update submodule work tree to invalid commit" '
prolog &&
reset_work_tree_to_interested add_sub1 &&
(
cd submodule_update &&
git branch -t invalid_sub1 origin/invalid_sub1 &&
test_must_fail $command invalid_sub1 &&
test_superproject_content origin/add_sub1 &&
test_submodule_content sub1 origin/add_sub1
)
'
# Updating a submodule from an invalid sha1 updates
test_expect_success "$command: modified submodule does not update submodule work tree from invalid commit" '
prolog &&
reset_work_tree_to_interested invalid_sub1 &&
(
cd submodule_update &&
git branch -t valid_sub1 origin/valid_sub1 &&
test_must_fail $command valid_sub1 &&
test_superproject_content origin/invalid_sub1
)
'
}

View File

@ -5,6 +5,14 @@ test_description='read-tree can handle submodules'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-submodule-update.sh

KNOWN_FAILURE_SUBMODULE_RECURSIVE_NESTED=1
KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS=1
KNOWN_FAILURE_SUBMODULE_OVERWRITE_IGNORED_UNTRACKED=1

test_submodule_switch_recursing "git read-tree --recurse-submodules -u -m"

test_submodule_forced_switch_recursing "git read-tree --recurse-submodules -u --reset"

test_submodule_switch "git read-tree -u -m"

test_submodule_forced_switch "git read-tree -u --reset"

View File

@ -63,6 +63,12 @@ test_expect_success '"checkout <submodule>" honors submodule.*.ignore from .git/
! test -s actual
'

KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS=1
KNOWN_FAILURE_SUBMODULE_RECURSIVE_NESTED=1
test_submodule_switch_recursing "git checkout --recurse-submodules"

test_submodule_forced_switch_recursing "git checkout -f --recurse-submodules"

test_submodule_switch "git checkout"

test_submodule_forced_switch "git checkout -f"

View File

@ -10,6 +10,8 @@
#include "attr.h"
#include "split-index.h"
#include "dir.h"
#include "submodule.h"
#include "submodule-config.h"

/*
* Error messages expected by scripts out of plumbing commands such as
@ -45,6 +47,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_ERROR_TYPES] = {

/* ERROR_WOULD_LOSE_ORPHANED_REMOVED */
"Working tree file '%s' would be removed by sparse checkout update.",

/* ERROR_WOULD_LOSE_SUBMODULE */
"Submodule '%s' cannot checkout new HEAD.",
};

#define ERRORMSG(o,type) \
@ -161,6 +166,8 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
_("The following working tree files would be overwritten by sparse checkout update:\n%s");
msgs[ERROR_WOULD_LOSE_ORPHANED_REMOVED] =
_("The following working tree files would be removed by sparse checkout update:\n%s");
msgs[ERROR_WOULD_LOSE_SUBMODULE] =
_("Submodule '%s' cannot checkout new HEAD");

opts->show_all_errors = 1;
/* rejected paths may not have a static buffer */
@ -240,12 +247,75 @@ static void display_error_msgs(struct unpack_trees_options *o)
fprintf(stderr, _("Aborting\n"));
}

static int check_submodule_move_head(const struct cache_entry *ce,
const char *old_id,
const char *new_id,
struct unpack_trees_options *o)
{
const struct submodule *sub = submodule_from_ce(ce);
if (!sub)
return 0;

switch (sub->update_strategy.type) {
case SM_UPDATE_UNSPECIFIED:
case SM_UPDATE_CHECKOUT:
if (submodule_move_head(ce->name, old_id, new_id, SUBMODULE_MOVE_HEAD_DRY_RUN))
return o->gently ? -1 :
add_rejected_path(o, ERROR_WOULD_LOSE_SUBMODULE, ce->name);
return 0;
case SM_UPDATE_NONE:
return 0;
case SM_UPDATE_REBASE:
case SM_UPDATE_MERGE:
case SM_UPDATE_COMMAND:
default:
warning(_("submodule update strategy not supported for submodule '%s'"), ce->name);
return -1;
}
}

static void reload_gitmodules_file(struct index_state *index,
struct checkout *state)
{
int i;
for (i = 0; i < index->cache_nr; i++) {
struct cache_entry *ce = index->cache[i];
if (ce->ce_flags & CE_UPDATE) {
int r = strcmp(ce->name, ".gitmodules");
if (r < 0)
continue;
else if (r == 0) {
submodule_free();
checkout_entry(ce, state, NULL);
gitmodules_config();
git_config(submodule_config, NULL);
} else
break;
}
}
}

/*
* Unlink the last component and schedule the leading directories for
* removal, such that empty directories get removed.
*/
static void unlink_entry(const struct cache_entry *ce)
{
const struct submodule *sub = submodule_from_ce(ce);
if (sub) {
switch (sub->update_strategy.type) {
case SM_UPDATE_UNSPECIFIED:
case SM_UPDATE_CHECKOUT:
case SM_UPDATE_REBASE:
case SM_UPDATE_MERGE:
submodule_move_head(ce->name, "HEAD", NULL,
SUBMODULE_MOVE_HEAD_FORCE);
break;
case SM_UPDATE_NONE:
case SM_UPDATE_COMMAND:
return; /* Do not touch the submodule. */
}
}
if (!check_leading_path(ce->name, ce_namelen(ce)))
return;
if (remove_or_warn(ce->ce_mode, ce->name))
@ -301,6 +371,9 @@ static int check_updates(struct unpack_trees_options *o)
remove_marked_cache_entries(index);
remove_scheduled_dirs();

if (should_update_submodules() && o->update && !o->dry_run)
reload_gitmodules_file(index, &state);

for (i = 0; i < index->cache_nr; i++) {
struct cache_entry *ce = index->cache[i];

@ -1358,17 +1431,26 @@ static int verify_uptodate_1(const struct cache_entry *ce,
if (!lstat(ce->name, &st)) {
int flags = CE_MATCH_IGNORE_VALID|CE_MATCH_IGNORE_SKIP_WORKTREE;
unsigned changed = ie_match_stat(o->src_index, ce, &st, flags);

if (submodule_from_ce(ce)) {
int r = check_submodule_move_head(ce,
"HEAD", oid_to_hex(&ce->oid), o);
if (r)
return o->gently ? -1 :
add_rejected_path(o, error_type, ce->name);
return 0;
}

if (!changed)
return 0;
/*
* NEEDSWORK: the current default policy is to allow
* submodule to be out of sync wrt the superproject
* index. This needs to be tightened later for
* submodules that are marked to be automatically
* checked out.
* Historic default policy was to allow submodule to be out
* of sync wrt the superproject index. If the submodule was
* not considered interesting above, we don't care here.
*/
if (S_ISGITLINK(ce->ce_mode))
return 0;

errno = 0;
}
if (errno == ENOENT)
@ -1407,11 +1489,16 @@ static void invalidate_ce_path(const struct cache_entry *ce,
* Currently, git does not checkout subprojects during a superproject
* checkout, so it is not going to overwrite anything.
*/
static int verify_clean_submodule(const struct cache_entry *ce,
static int verify_clean_submodule(const char *old_sha1,
const struct cache_entry *ce,
enum unpack_trees_error_types error_type,
struct unpack_trees_options *o)
{
return 0;
if (!submodule_from_ce(ce))
return 0;

return check_submodule_move_head(ce, old_sha1,
oid_to_hex(&ce->oid), o);
}

static int verify_clean_subdirectory(const struct cache_entry *ce,
@ -1427,16 +1514,18 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,
struct dir_struct d;
char *pathbuf;
int cnt = 0;
unsigned char sha1[20];

if (S_ISGITLINK(ce->ce_mode) &&
resolve_gitlink_ref(ce->name, "HEAD", sha1) == 0) {
/* If we are not going to update the submodule, then
if (S_ISGITLINK(ce->ce_mode)) {
unsigned char sha1[20];
int sub_head = resolve_gitlink_ref(ce->name, "HEAD", sha1);
/*
* If we are not going to update the submodule, then
* we don't care.
*/
if (!hashcmp(sha1, ce->oid.hash))
if (!sub_head && !hashcmp(sha1, ce->oid.hash))
return 0;
return verify_clean_submodule(ce, error_type, o);
return verify_clean_submodule(sub_head ? NULL : sha1_to_hex(sha1),
ce, error_type, o);
}

/*
@ -1575,9 +1664,15 @@ static int verify_absent_1(const struct cache_entry *ce,
path = xmemdupz(ce->name, len);
if (lstat(path, &st))
ret = error_errno("cannot stat '%s'", path);
else
ret = check_ok_to_remove(path, len, DT_UNKNOWN, NULL,
&st, error_type, o);
else {
if (submodule_from_ce(ce))
ret = check_submodule_move_head(ce,
oid_to_hex(&ce->oid),
NULL, o);
else
ret = check_ok_to_remove(path, len, DT_UNKNOWN, NULL,
&st, error_type, o);
}
free(path);
return ret;
} else if (lstat(ce->name, &st)) {
@ -1585,6 +1680,10 @@ static int verify_absent_1(const struct cache_entry *ce,
return error_errno("cannot stat '%s'", ce->name);
return 0;
} else {
if (submodule_from_ce(ce))
return check_submodule_move_head(ce, oid_to_hex(&ce->oid),
NULL, o);

return check_ok_to_remove(ce->name, ce_namelen(ce),
ce_to_dtype(ce), ce, &st,
error_type, o);
@ -1640,6 +1739,15 @@ static int merged_entry(const struct cache_entry *ce,
return -1;
}
invalidate_ce_path(merge, o);

if (submodule_from_ce(ce)) {
int ret = check_submodule_move_head(ce, NULL,
oid_to_hex(&ce->oid),
o);
if (ret)
return ret;
}

} else if (!(old->ce_flags & CE_CONFLICTED)) {
/*
* See if we can re-use the old CE directly?
@ -1660,6 +1768,14 @@ static int merged_entry(const struct cache_entry *ce,
update |= old->ce_flags & (CE_SKIP_WORKTREE | CE_NEW_SKIP_WORKTREE);
invalidate_ce_path(old, o);
}

if (submodule_from_ce(ce)) {
int ret = check_submodule_move_head(ce, oid_to_hex(&old->oid),
oid_to_hex(&ce->oid),
o);
if (ret)
return ret;
}
} else {
/*
* Previously unmerged entry left as an existence

View File

@ -21,6 +21,7 @@ enum unpack_trees_error_types {
ERROR_SPARSE_NOT_UPTODATE_FILE,
ERROR_WOULD_LOSE_ORPHANED_OVERWRITTEN,
ERROR_WOULD_LOSE_ORPHANED_REMOVED,
ERROR_WOULD_LOSE_SUBMODULE,
NB_UNPACK_TREES_ERROR_TYPES
};