Merge branch 'sb/checkout-recurse-submodules'
authorJunio C Hamano <gitster@pobox.com>
Tue, 28 Mar 2017 21:05:58 +0000 (14:05 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 28 Mar 2017 21:05:58 +0000 (14:05 -0700)
"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

17 files changed:
Documentation/git-checkout.txt
Documentation/git-read-tree.txt
builtin/checkout.c
builtin/grep.c
builtin/read-tree.c
builtin/submodule--helper.c
dir.c
entry.c
submodule-config.c
submodule-config.h
submodule.c
submodule.h
t/lib-submodule-update.sh
t/t1013-read-tree-submodule.sh
t/t2013-checkout-submodule.sh
unpack-trees.c
unpack-trees.h
index 8e2c0662ddd72c1bc0765aadbbafd22b62b5adf6..d6399c0af86bb84cd86b82500ce706cbcd93cd93 100644 (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
index fa1d557e5b98eb6dc40e3baa65964138b4cc0621..ed9d63ef4a35fa5ba70b1b7f1ffd393f517be030 100644 (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.
index 81f07c3ef271db08e36ee7a89b1d128d099ecb96..3ecc47bec07b47f3452b4744f0e7482681e61748 100644 (file)
 #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"));
 
index a9e82dc975e007fc2a77240ea1f9dca0803ecf32..3f3efa95de2570bcd4f51a29fc7936817b9cee23 100644 (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.
index 8ba64bc785670590897638c8e58172c74556189d..23e212ee8c5b2d26f03ac6be2b822a87ad06c233 100644 (file)
 #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?");
index 15a5430c0028f45ff6265a821ff85aa9d8445230..be316bbbc898c9311c6ec82aa0cfbc808f04e8c9 100644 (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);
-
-       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);
+       /* Connect module worktree and git dir */
+       connect_work_tree_and_git_dir(path, sm_gitdir);
 
-       /* 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);
diff --git a/dir.c b/dir.c
index 837ff965a470e74028fce6019f4584e55dcf445c..f451bfa48c0a0e7905d4c2adf4e3e05a8d272a8a 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -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",
-                  relative_path(git_dir, work_tree, &rel_path));
+       /* 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);
diff --git a/entry.c b/entry.c
index c6eea240b69eae1a8b19eb61f6bbf0c888c5e1cb..d2b512da90a3cc84d592a2024cc8dac363c9418d 100644 (file)
--- a/entry.c
+++ b/entry.c
@@ -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) {
index bb069bc09734f473598d78493c1c08b1b4bd85e2..4f58491ddb0705ab56e238006199d35db19dfb5a 100644 (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)
 {
index 70f19363fd8bf5b7f42e974dacdc3ed2cd58a96a..d434ecdb45c074dd386405f20ed73ac1c8b646bd 100644 (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 */
index 3200b7bb2b2360c7efab86b680ed5d0cafa5e9d3..040f4c2287190cca706f9cd007c5942ae5c2b117 100644 (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 @@ int bad_to_remove_submodule(const char *path, unsigned flags)
        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, '/');
 
index c8a0c9cb2971219b807f7fe931c4dc5355b978ea..8a8bc49dc9626b9ba4112f3b0bf25b212d864d1d 100644 (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
index 915eb4a7c65ca5c574beddc676bf0115990eec3f..fb4f7b014e10cd383ed42550f77a14ea9b0211ac 100755 (executable)
@@ -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 =>
 # - 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
+               )
+       '
+}
index 20526aed34212597179eff4bec41866ae3254183..de1ba02dc5b1c76ea52eb490298bcfcefc342b51 100755 (executable)
@@ -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"
index 6847f7582234d656563f867976a64142a3a01621..e8f70b806f110c75b9bda5bca0af6732be391fcd 100755 (executable)
@@ -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"
index 3a8ee19fe837aae6939c3a6b902f6b067dcde9e4..8333da2cc908d7ea373e3d67fbf3ac6526d9019d 100644 (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
index 36a73a6d003b6906dfb402aa930b352f5b368c60..6c48117b845fbf7b983852be302e4472c5e6d651 100644 (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
 };