When working with submodules it is easy to forget to push a submodule to the server but pushing a super-project that contains a commit for that submodule. The result is that the superproject points at a submodule commit that is not avaliable on the server. Check that all submodule commits that are about to be pushed are present on a remote of the submodule and require forcing if that is not the case. This does not guarantee that all submodules a super-project needs will be available on the server. In that case both the super-project and the submodules would need an atomic push. This does however prevent the human error of forgetting to push a submodule. Signed-off-by: Fredrik Gustafsson <iveqy@xxxxxxxxx> Mentored-by: Jens Lehmann <Jens.Lehmann@xxxxxx> Mentored-by: Heiko Voigt <hvoigt@xxxxxxxxxx> --- submodule.c | 115 ++++++++++++++++++++++++++++++++++++++++ submodule.h | 1 + t/t5531-deep-submodule-push.sh | 54 ++++++++++++++++++- transport.c | 8 +++ 4 files changed, 175 insertions(+), 3 deletions(-) diff --git a/submodule.c b/submodule.c index 1ba9646..3820f1b 100644 --- a/submodule.c +++ b/submodule.c @@ -308,6 +308,121 @@ void set_config_fetch_recurse_submodules(int value) config_fetch_recurse_submodules = value; } +static int is_submodule_commit_pushed(const char *path, const unsigned char sha1[20]) +{ + int is_pushed = 0; + if (!add_submodule_odb(path) && lookup_commit_reference(sha1)) { + struct child_process cp; + const char *argv[] = {"rev-list", NULL, "--not", "--remotes", "-n", "1" , NULL}; + struct strbuf buf = STRBUF_INIT; + + argv[1] = sha1_to_hex(sha1); + memset(&cp, 0, sizeof(cp)); + cp.argv = argv; + cp.env = local_repo_env; + cp.git_cmd = 1; + cp.no_stdin = 1; + cp.out = -1; + cp.dir = path; + if (!run_command(&cp) && !strbuf_read(&buf, cp.out, 41)) + is_pushed = 1; + close(cp.out); + strbuf_release(&buf); + } + return is_pushed; +} + +static void collect_unpushed_submodules_from_revs(struct diff_queue_struct *q, + struct diff_options *options, + void *data) +{ + int i; + int *found_unpushed_submodule = data; + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (!S_ISGITLINK(p->two->mode)) + continue; + if (!is_submodule_commit_pushed(p->two->path, p->two->sha1)) { + *found_unpushed_submodule = 1; + break; + } + } +} + +static int collect_unpushed_submodules_in_tree(const unsigned char *sha1, const char *base, int baselen, + const char *pathname, unsigned mode, int stage, void *context) +{ + int *found_unpushed_submodules = context; + struct strbuf path = STRBUF_INIT; + + strbuf_add(&path,base,strlen(base)); + strbuf_add(&path,pathname,strlen(pathname)); + + if (S_ISGITLINK(mode)) { + if(!is_submodule_commit_pushed(path.buf,sha1)) + *found_unpushed_submodules = 1; + return 0; + } + return READ_TREE_RECURSIVE; +} + +static void parent_commits_pushed(struct commit *commit, struct commit_list *parent, int *found_unpushed_submodule) +{ + while (parent) { + struct diff_options diff_opts; + diff_setup(&diff_opts); + DIFF_OPT_SET(&diff_opts, RECURSIVE); + diff_opts.output_format |= DIFF_FORMAT_CALLBACK; + diff_opts.format_callback = collect_unpushed_submodules_from_revs; + diff_opts.format_callback_data = found_unpushed_submodule; + if (diff_setup_done(&diff_opts) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(parent->item->object.sha1, commit->object.sha1, "", &diff_opts); + diffcore_std(&diff_opts); + diff_flush(&diff_opts); + parent = parent->next; + } +} + +static void tree_commits_pushed(struct commit *commit, int *found_unpushed_submodule) +{ + struct tree * tree; + struct pathspec pathspec; + tree = parse_tree_indirect(commit->object.sha1); + init_pathspec(&pathspec,NULL); + read_tree_recursive(tree, "", 0, 0, &pathspec, collect_unpushed_submodules_in_tree, + found_unpushed_submodule); +} + +int check_for_unpushed_submodule_commits(unsigned char new_sha1[20]) +{ + struct rev_info rev; + struct commit *commit; + const char *argv[] = {NULL, NULL, "--not", "--remotes", NULL}; + int argc = ARRAY_SIZE(argv) - 1; + char *sha1_copy; + int found_unpushed_submodule = 0; + + init_revisions(&rev, NULL); + sha1_copy = xstrdup(sha1_to_hex(new_sha1)); + argv[1] = sha1_copy; + setup_revisions(argc, argv, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + while ((commit = get_revision(&rev)) && !found_unpushed_submodule) { + struct commit_list *parent = commit->parents; + if(parent) + parent_commits_pushed(commit,parent,&found_unpushed_submodule); + else + tree_commits_pushed(commit,&found_unpushed_submodule); + } + + free(sha1_copy); + return found_unpushed_submodule; +} + static int is_submodule_commit_present(const char *path, unsigned char sha1[20]) { int is_present = 0; diff --git a/submodule.h b/submodule.h index 5350b0d..0a4d395 100644 --- a/submodule.h +++ b/submodule.h @@ -29,5 +29,6 @@ int fetch_populated_submodules(int num_options, const char **options, unsigned is_submodule_modified(const char *path, int ignore_untracked); int merge_submodule(unsigned char result[20], const char *path, const unsigned char base[20], const unsigned char a[20], const unsigned char b[20]); +int check_for_unpushed_submodule_commits(unsigned char sha1[20]); #endif diff --git a/t/t5531-deep-submodule-push.sh b/t/t5531-deep-submodule-push.sh index 0b55466..15474c1 100755 --- a/t/t5531-deep-submodule-push.sh +++ b/t/t5531-deep-submodule-push.sh @@ -32,7 +32,7 @@ test_expect_success push ' ) ' -test_expect_failure 'push fails if submodule has no remote' ' +test_expect_success 'push fails if submodule has no remote' ' ( cd work/gar/bage && >junk2 && @@ -47,7 +47,7 @@ test_expect_failure 'push fails if submodule has no remote' ' ) ' -test_expect_failure 'push fails if submodule commit not on remote' ' +test_expect_success 'push fails if submodule commit not on remote' ' ( cd work/gar && git clone --bare bage ../../submodule.git && @@ -66,7 +66,7 @@ test_expect_failure 'push fails if submodule commit not on remote' ' ) ' -test_expect_failure 'push succeeds after commit was pushed to remote' ' +test_expect_success 'push succeeds after commit was pushed to remote' ' ( cd work/gar/bage && git push origin master @@ -76,4 +76,52 @@ test_expect_failure 'push succeeds after commit was pushed to remote' ' git push ../pub.git master ) ' + +test_expect_success 'push fails when commit on multiple branches if one branch has no remote' ' + ( + cd work/gar/bage && + >junk4 && + git add junk4 && + git commit -m "Fourth junk" + ) && + ( + cd work && + git branch branch2 && + git add gar/bage && + git commit -m "Fourth commit for gar/bage" && + git checkout branch2 && + ( + cd gar/bage && + git checkout HEAD~1 + ) && + >junk1 && + git add junk1 && + git commit -m "First junk" && + test_must_fail git push ../pub.git + ) +' + +test_expect_success 'push fails if submodule has no remote and is on the first superproject commit' ' + mkdir a && + ( + cd a && + git init --bare + ) && + git clone a a1 && + ( + cd a1 && + mkdir b && + ( + cd b && + git init && + >junk && + git add junk && + git commit -m "initial" + ) && + git add b && + git commit -m "added submodule" && + test_must_fail git push origin master + ) +' + test_done diff --git a/transport.c b/transport.c index c9c8056..e0fd435 100644 --- a/transport.c +++ b/transport.c @@ -10,6 +10,7 @@ #include "refs.h" #include "branch.h" #include "url.h" +#include "submodule.h" /* rsync support */ @@ -1041,6 +1042,13 @@ int transport_push(struct transport *transport, flags & TRANSPORT_PUSH_MIRROR, flags & TRANSPORT_PUSH_FORCE); + if(!(flags & TRANSPORT_PUSH_FORCE) && !is_bare_repository()) { + struct ref *ref = remote_refs; + for (; ref; ref = ref->next) + if(!is_null_sha1(ref->new_sha1) && check_for_unpushed_submodule_commits(ref->new_sha1)) + die("There are unpushed submodules, aborting. Use -f to force a push"); + } + push_ret = transport->push_refs(transport, remote_refs, flags); err = push_had_errors(remote_refs); ret = push_ret | err; -- 1.7.6.236.g7ad21 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html