diff --git a/CHANGELOG.md b/CHANGELOG.md index b01f48138..b8304b0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,3 +145,9 @@ v0.21 + 1 * git_treebuilder_create now takes a repository so that it can query repository configuration. Subsequently, git_treebuilder_write no longer takes a repository. + +* The git_submodule_update function was renamed to + git_submodule_update_strategy. git_submodule_update is now used to + provide functionalty similar to "git submodule update". + + diff --git a/include/git2/submodule.h b/include/git2/submodule.h index 8efc26e79..31c68e3b9 100644 --- a/include/git2/submodule.h +++ b/include/git2/submodule.h @@ -10,6 +10,8 @@ #include "common.h" #include "types.h" #include "oid.h" +#include "remote.h" +#include "checkout.h" /** * @file git2/submodule.h @@ -105,6 +107,68 @@ typedef enum { GIT_SUBMODULE_STATUS_WD_WD_MODIFIED | \ GIT_SUBMODULE_STATUS_WD_UNTRACKED)) != 0) +/** + * Submodule update options structure + * + * Use the GIT_SUBMODULE_UPDATE_OPTIONS_INIT to get the default settings, like this: + * + * git_submodule_update_options opts = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + */ +typedef struct git_submodule_update_options { + unsigned int version; + + /** + * These options are passed to the checkout step. To disable + * checkout, set the `checkout_strategy` to + * `GIT_CHECKOUT_NONE`. Generally you will want the use + * GIT_CHECKOUT_SAFE to update files in the working + * directory. Use the `clone_checkout_strategy` field + * to set the checkout strategy that will be used in + * the case where update needs to clone the repository. + */ + git_checkout_options checkout_opts; + + /** + * Callbacks to use for reporting fetch progress, and for acquiring + * credentials in the event they are needed. + */ + git_remote_callbacks remote_callbacks; + + /** + * The checkout strategy to use when the sub repository needs to + * be cloned. Use GIT_CHECKOUT_SAFE_CREATE to create all files + * in the working directory for the newly cloned repository. + */ + unsigned int clone_checkout_strategy; + + /** + * The identity used when updating the reflog. NULL means to + * use the default signature using the config. + */ + git_signature *signature; +} git_submodule_update_options; + +#define GIT_SUBMODULE_UPDATE_OPTIONS_VERSION 1 +#define GIT_SUBMODULE_UPDATE_OPTIONS_INIT {GIT_CHECKOUT_OPTIONS_VERSION, {GIT_CHECKOUT_OPTIONS_VERSION, GIT_CHECKOUT_SAFE}, GIT_REMOTE_CALLBACKS_INIT, GIT_CHECKOUT_SAFE_CREATE} + +/** + * Update a submodule. This will clone a missing submodule and + * checkout the subrepository to the commit specified in the index of + * containing repository. + * + * @param submodule Submodule object + * @param init If the submodule is not initialized, setting this flag to true + * will initialize the submodule before updating. Otherwise, this will + * return an error if attempting to update an uninitialzed repository. + * but setting this to true forces them to be updated. + * @param options configuration options for the update. If NULL, the + * function works as though GIT_SUBMODULE_UPDATE_OPTIONS_INIT was passed. + * @return 0 on success, any non-zero return value from a callback + * function, or a negative value to indicate an error (use + * `giterr_last` for a detailed error message). + */ +GIT_EXTERN(int) git_submodule_update(git_submodule *submodule, int init, git_submodule_update_options *options); + /** * Lookup submodule information by name or path. * @@ -403,7 +467,7 @@ GIT_EXTERN(git_submodule_ignore_t) git_submodule_set_ignore( * @return The current git_submodule_update_t value that will be used * for this submodule. */ -GIT_EXTERN(git_submodule_update_t) git_submodule_update( +GIT_EXTERN(git_submodule_update_t) git_submodule_update_strategy( git_submodule *submodule); /** diff --git a/src/submodule.c b/src/submodule.c index d2af1440a..d89dd049f 100644 --- a/src/submodule.c +++ b/src/submodule.c @@ -475,7 +475,7 @@ int git_submodule_repo_init( /* get the configured remote url of the submodule */ if ((error = git_buf_printf(&buf, "submodule.%s.url", sm->name)) < 0 || - (error = git_repository_config(&cfg, sm->repo)) < 0 || + (error = git_repository_config_snapshot(&cfg, sm->repo)) < 0 || (error = git_config_get_string(&configured_url, cfg, buf.ptr)) < 0 || (error = submodule_repo_init(&sub_repo, sm->repo, sm->path, configured_url, use_gitlink)) < 0) goto done; @@ -790,7 +790,7 @@ git_submodule_ignore_t git_submodule_set_ignore( return old; } -git_submodule_update_t git_submodule_update(git_submodule *submodule) +git_submodule_update_t git_submodule_update_strategy(git_submodule *submodule) { assert(submodule); return (submodule->update < GIT_SUBMODULE_UPDATE_CHECKOUT) ? @@ -835,6 +835,178 @@ git_submodule_recurse_t git_submodule_set_fetch_recurse_submodules( return old; } +static int submodule_repo_create( + git_repository **out, + git_repository *parent_repo, + const char *path) +{ + int error = 0; + git_buf workdir = GIT_BUF_INIT, repodir = GIT_BUF_INIT; + git_repository_init_options initopt = GIT_REPOSITORY_INIT_OPTIONS_INIT; + git_repository *subrepo = NULL; + + initopt.flags = + GIT_REPOSITORY_INIT_MKPATH | + GIT_REPOSITORY_INIT_NO_REINIT | + GIT_REPOSITORY_INIT_NO_DOTGIT_DIR | + GIT_REPOSITORY_INIT_RELATIVE_GITLINK; + + /* Workdir: path to sub-repo working directory */ + error = git_buf_joinpath(&workdir, git_repository_workdir(parent_repo), path); + if (error < 0) + goto cleanup; + + initopt.workdir_path = workdir.ptr; + + /** + * Repodir: path to the sub-repo. sub-repo goes in: + * /modules// with a gitlink in the + * sub-repo workdir directory to that repository. + */ + error = git_buf_join3( + &repodir, '/', git_repository_path(parent_repo), "modules", path); + if (error < 0) + goto cleanup; + + error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); + +cleanup: + git_buf_free(&workdir); + git_buf_free(&repodir); + + *out = subrepo; + + return error; +} + +/** + * Callback to override sub-repository creation when + * cloning a sub-repository. + */ +static int git_submodule_update_repo_init_cb( + git_repository **out, + const char *path, + int bare, + void *payload) +{ + GIT_UNUSED(bare); + git_submodule *sm = payload; + + return submodule_repo_create(out, sm->repo, path); +} + +int git_submodule_update(git_submodule *sm, int init, git_submodule_update_options *_update_options) +{ + int error; + unsigned int submodule_status; + git_config *config = NULL; + const char *submodule_url; + git_repository *sub_repo = NULL; + git_remote *remote = NULL; + git_object *target_commit = NULL; + git_buf buf = GIT_BUF_INIT; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + git_clone_options clone_options = GIT_CLONE_OPTIONS_INIT; + + assert(sm); + + if (_update_options) + memcpy(&update_options, _update_options, sizeof(git_submodule_update_options)); + + GITERR_CHECK_VERSION(&update_options, GIT_SUBMODULE_UPDATE_OPTIONS_VERSION, "git_submodule_update_options"); + + /* Copy over the remote callbacks */ + clone_options.remote_callbacks = update_options.remote_callbacks; + clone_options.signature = update_options.signature; + + /* Get the status of the submodule to determine if it is already initialized */ + if ((error = git_submodule_status(&submodule_status, sm)) < 0) + goto done; + + /* + * If submodule work dir is not already initialized, check to see + * what we need to do (initialize, clone, return error...) + */ + if (submodule_status & GIT_SUBMODULE_STATUS_WD_UNINITIALIZED) { + /* + * Work dir is not initialized, check to see if the submodule + * info has been copied into .git/config + */ + if ((error = git_repository_config_snapshot(&config, sm->repo)) < 0 || + (error = git_buf_printf(&buf, "submodule.%s.url", git_submodule_name(sm))) < 0) + goto done; + + if ((error = git_config_get_string(&submodule_url, config, git_buf_cstr(&buf))) < 0) { + /* + * If the error is not "not found" or if it is "not found" and we are not + * initializing the submodule, then return error. + */ + if (error != GIT_ENOTFOUND) + goto done; + + if (error == GIT_ENOTFOUND && !init) { + giterr_set(GITERR_SUBMODULE, "Submodule is not initialized."); + error = GIT_ERROR; + goto done; + } + + /* The submodule has not been initialized yet - initialize it now.*/ + if ((error = git_submodule_init(sm, 0)) < 0) + goto done; + + git_config_free(config); + config = NULL; + + if ((error = git_repository_config_snapshot(&config, sm->repo)) < 0 || + (error = git_config_get_string(&submodule_url, config, git_buf_cstr(&buf))) < 0) + goto done; + } + + /** submodule is initialized - now clone it **/ + /* override repo creation */ + clone_options.repository_cb = git_submodule_update_repo_init_cb; + clone_options.repository_cb_payload = sm; + + /* + * Do not perform checkout as part of clone, instead we + * will checkout the specific commit manually. + */ + clone_options.checkout_opts.checkout_strategy = GIT_CHECKOUT_NONE; + update_options.checkout_opts.checkout_strategy = update_options.clone_checkout_strategy; + + if ((error = git_clone(&sub_repo, submodule_url, sm->path, &clone_options)) < 0 || + (error = git_repository_set_head_detached(sub_repo, git_submodule_index_id(sm), update_options.signature, NULL)) < 0 || + (error = git_checkout_head(sub_repo, &update_options.checkout_opts)) != 0) + goto done; + } else { + /** + * Work dir is initialized - look up the commit in the parent repository's index, + * update the workdir contents of the subrepository, and set the subrepository's + * head to the new commit. + */ + if ((error = git_submodule_open(&sub_repo, sm)) < 0 || + (error = git_object_lookup(&target_commit, sub_repo, git_submodule_index_id(sm), GIT_OBJ_COMMIT)) < 0 || + (error = git_checkout_tree(sub_repo, target_commit, &update_options.checkout_opts)) != 0 || + (error = git_repository_set_head_detached(sub_repo, git_submodule_index_id(sm), update_options.signature, NULL)) < 0) + goto done; + + /* Invalidate the wd flags as the workdir has been updated. */ + sm->flags = sm->flags & + ~(GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_OID_VALID | + GIT_SUBMODULE_STATUS__WD_SCANNED); + } + +done: + git_buf_free(&buf); + git_config_free(config); + git_object_free(target_commit); + git_remote_free(remote); + git_repository_free(sub_repo); + + return error; +} + int git_submodule_init(git_submodule *sm, int overwrite) { int error; @@ -853,7 +1025,7 @@ int git_submodule_init(git_submodule *sm, int overwrite) /* write "submodule.NAME.url" */ - if ((git_submodule_resolve_url(&effective_submodule_url, sm->repo, sm->url)) < 0 || + if ((error = git_submodule_resolve_url(&effective_submodule_url, sm->repo, sm->url)) < 0 || (error = git_buf_printf(&key, "submodule.%s.url", sm->name)) < 0 || (error = git_config__update_entry( cfg, key.ptr, effective_submodule_url.ptr, overwrite != 0, false)) < 0) @@ -1867,16 +2039,31 @@ static int lookup_head_remote_key(git_buf *remote_name, git_repository *repo) if ((error = git_repository_head(&head, repo)) < 0) return error; - /* lookup remote tracking branch of HEAD */ - if (!(error = git_branch_upstream_name( - &upstream_name, repo, git_reference_name(head)))) - { - /* lookup remote of remote tracking branch */ - error = git_branch_remote_name(remote_name, repo, upstream_name.ptr); - - git_buf_free(&upstream_name); + /** + * If head does not refer to a branch, then return + * GIT_ENOTFOUND to indicate that we could not find + * a remote key for the local tracking branch HEAD points to. + **/ + if (!git_reference_is_branch(head)) { + giterr_set(GITERR_INVALID, + "HEAD does not refer to a branch."); + error = GIT_ENOTFOUND; + goto done; } + /* lookup remote tracking branch of HEAD */ + if ((error = git_branch_upstream_name( + &upstream_name, + repo, + git_reference_name(head))) < 0) + goto done; + + /* lookup remote of remote tracking branch */ + if ((error = git_branch_remote_name(remote_name, repo, upstream_name.ptr)) < 0) + goto done; + +done: + git_buf_free(&upstream_name); git_reference_free(head); return error; diff --git a/tests/submodule/init.c b/tests/submodule/init.c index c03bf4610..d07bc9a5b 100644 --- a/tests/submodule/init.c +++ b/tests/submodule/init.c @@ -71,3 +71,41 @@ void test_submodule_init__relative_url(void) git_buf_free(&absolute_url); git_config_free(cfg); } + +void test_submodule_init__relative_url_detached_head(void) +{ + git_submodule *sm; + git_config *cfg; + git_buf absolute_url = GIT_BUF_INIT; + const char *config_url; + git_reference *head_ref = NULL; + git_object *head_commit = NULL; + + g_repo = setup_fixture_submodule_simple(); + + /* Put the parent repository into a detached head state. */ + cl_git_pass(git_repository_head(&head_ref, g_repo)); + cl_git_pass(git_reference_peel(&head_commit, head_ref, GIT_OBJ_COMMIT)); + + cl_git_pass(git_repository_set_head_detached(g_repo, git_commit_id((git_commit *)head_commit), NULL, NULL)); + + cl_assert(git_path_dirname_r(&absolute_url, git_repository_workdir(g_repo)) > 0); + cl_git_pass(git_buf_joinpath(&absolute_url, absolute_url.ptr, "testrepo.git")); + + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + + /* verify that the .gitmodules is set with an absolute path*/ + cl_assert_equal_s("../testrepo.git", git_submodule_url(sm)); + + /* init and verify that absolute path is written to .git/config */ + cl_git_pass(git_submodule_init(sm, false)); + + cl_git_pass(git_repository_config(&cfg, g_repo)); + + git_config_get_string(&config_url, cfg, "submodule.testrepo.url"); + cl_assert_equal_s(absolute_url.ptr, config_url); + + git_buf_free(&absolute_url); + git_config_free(cfg); + +} diff --git a/tests/submodule/lookup.c b/tests/submodule/lookup.c index 34de5923e..fa452fb82 100644 --- a/tests/submodule/lookup.c +++ b/tests/submodule/lookup.c @@ -49,7 +49,7 @@ void test_submodule_lookup__accessors(void) cl_assert(git_oid_streq(git_submodule_wd_id(sm), oid) == 0); cl_assert(git_submodule_ignore(sm) == GIT_SUBMODULE_IGNORE_NONE); - cl_assert(git_submodule_update(sm) == GIT_SUBMODULE_UPDATE_CHECKOUT); + cl_assert(git_submodule_update_strategy(sm) == GIT_SUBMODULE_UPDATE_CHECKOUT); git_submodule_free(sm); diff --git a/tests/submodule/modify.c b/tests/submodule/modify.c index 582d4166b..9bb48bad2 100644 --- a/tests/submodule/modify.c +++ b/tests/submodule/modify.c @@ -160,7 +160,7 @@ void test_submodule_modify__edit_and_save(void) cl_assert_equal_i( GIT_SUBMODULE_IGNORE_UNTRACKED, git_submodule_ignore(sm1)); cl_assert_equal_i( - GIT_SUBMODULE_UPDATE_REBASE, git_submodule_update(sm1)); + GIT_SUBMODULE_UPDATE_REBASE, git_submodule_update_strategy(sm1)); cl_assert_equal_i( GIT_SUBMODULE_RECURSE_YES, git_submodule_fetch_recurse_submodules(sm1)); @@ -179,7 +179,7 @@ void test_submodule_modify__edit_and_save(void) /* check that revert was successful */ cl_assert_equal_s(old_url, git_submodule_url(sm1)); cl_assert_equal_i((int)old_ignore, (int)git_submodule_ignore(sm1)); - cl_assert_equal_i((int)old_update, (int)git_submodule_update(sm1)); + cl_assert_equal_i((int)old_update, (int)git_submodule_update_strategy(sm1)); cl_assert_equal_i( old_fetchrecurse, git_submodule_fetch_recurse_submodules(sm1)); @@ -202,7 +202,7 @@ void test_submodule_modify__edit_and_save(void) cl_assert_equal_i( (int)GIT_SUBMODULE_IGNORE_UNTRACKED, (int)git_submodule_ignore(sm1)); cl_assert_equal_i( - (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update(sm1)); + (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update_strategy(sm1)); cl_assert_equal_i(GIT_SUBMODULE_RECURSE_YES, git_submodule_fetch_recurse_submodules(sm1)); /* call reload and check that the new values are loaded */ @@ -212,7 +212,7 @@ void test_submodule_modify__edit_and_save(void) cl_assert_equal_i( (int)GIT_SUBMODULE_IGNORE_UNTRACKED, (int)git_submodule_ignore(sm1)); cl_assert_equal_i( - (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update(sm1)); + (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update_strategy(sm1)); cl_assert_equal_i(GIT_SUBMODULE_RECURSE_YES, git_submodule_fetch_recurse_submodules(sm1)); /* open a second copy of the repo and compare submodule */ @@ -223,7 +223,7 @@ void test_submodule_modify__edit_and_save(void) cl_assert_equal_i( GIT_SUBMODULE_IGNORE_UNTRACKED, git_submodule_ignore(sm2)); cl_assert_equal_i( - GIT_SUBMODULE_UPDATE_REBASE, git_submodule_update(sm2)); + GIT_SUBMODULE_UPDATE_REBASE, git_submodule_update_strategy(sm2)); cl_assert_equal_i( GIT_SUBMODULE_RECURSE_NO, git_submodule_fetch_recurse_submodules(sm2)); diff --git a/tests/submodule/update.c b/tests/submodule/update.c new file mode 100644 index 000000000..ebf864d9f --- /dev/null +++ b/tests/submodule/update.c @@ -0,0 +1,380 @@ +#include "clar_libgit2.h" +#include "posix.h" +#include "path.h" +#include "submodule_helpers.h" +#include "fileops.h" + +static git_repository *g_repo = NULL; + +void test_submodule_update__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_submodule_update__unitialized_submodule_no_init(void) +{ + git_submodule *sm; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + + g_repo = setup_fixture_submodule_simple(); + + /* get the submodule */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + + /* updating an unitialized repository throws */ + cl_git_fail_with( + GIT_ERROR, + git_submodule_update(sm, 0, &update_options)); + + git_submodule_free(sm); +} + +struct update_submodule_cb_payload { + int update_tips_called; + int checkout_progress_called; + int checkout_notify_called; +}; + +static void checkout_progress_cb( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload) +{ + struct update_submodule_cb_payload *update_payload = payload; + + GIT_UNUSED(path); + GIT_UNUSED(completed_steps); + GIT_UNUSED(total_steps); + + update_payload->checkout_progress_called = 1; +} + +static int checkout_notify_cb( + git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload) +{ + struct update_submodule_cb_payload *update_payload = payload; + + GIT_UNUSED(why); + GIT_UNUSED(path); + GIT_UNUSED(baseline); + GIT_UNUSED(target); + GIT_UNUSED(workdir); + + update_payload->checkout_notify_called = 1; + + return 0; +} + +static int update_tips(const char *refname, const git_oid *a, const git_oid *b, void *data) +{ + struct update_submodule_cb_payload *update_payload = data; + + GIT_UNUSED(refname); + GIT_UNUSED(a); + GIT_UNUSED(b); + + update_payload->update_tips_called = 1; + + return 1; +} + +void test_submodule_update__update_submodule(void) +{ + git_submodule *sm; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + unsigned int submodule_status = 0; + struct update_submodule_cb_payload update_payload = { 0 }; + + g_repo = setup_fixture_submodule_simple(); + + update_options.checkout_opts.progress_cb = checkout_progress_cb; + update_options.checkout_opts.progress_payload = &update_payload; + + update_options.remote_callbacks.update_tips = update_tips; + update_options.remote_callbacks.payload = &update_payload; + + /* get the submodule */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + + /* verify the initial state of the submodule */ + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_WD_UNINITIALIZED); + + /* initialize and update the submodule */ + cl_git_pass(git_submodule_init(sm, 0)); + cl_git_pass(git_submodule_update(sm, 0, &update_options)); + + /* verify state */ + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_IN_WD); + + cl_assert(git_oid_streq(git_submodule_head_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + + /* verify that the expected callbacks have been called. */ + cl_assert_equal_i(1, update_payload.checkout_progress_called); + cl_assert_equal_i(1, update_payload.update_tips_called); + + git_submodule_free(sm); +} + +void test_submodule_update__update_and_init_submodule(void) +{ + git_submodule *sm; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + unsigned int submodule_status = 0; + + g_repo = setup_fixture_submodule_simple(); + + /* get the submodule */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_WD_UNINITIALIZED); + + /* update (with option to initialize sub repo) */ + cl_git_pass(git_submodule_update(sm, 1, &update_options)); + + /* verify expected state */ + cl_assert(git_oid_streq(git_submodule_head_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + + git_submodule_free(sm); +} + +void test_submodule_update__update_already_checked_out_submodule(void) +{ + git_submodule *sm = NULL; + git_checkout_options checkout_options = GIT_CHECKOUT_OPTIONS_INIT; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + unsigned int submodule_status = 0; + git_reference *branch_reference = NULL; + git_object *branch_commit = NULL; + struct update_submodule_cb_payload update_payload = { 0 }; + + g_repo = setup_fixture_submodule_simple(); + + update_options.checkout_opts.progress_cb = checkout_progress_cb; + update_options.checkout_opts.progress_payload = &update_payload; + + /* Initialize and update the sub repository */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_WD_UNINITIALIZED); + + cl_git_pass(git_submodule_update(sm, 1, &update_options)); + + /* verify expected state */ + cl_assert(git_oid_streq(git_submodule_head_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + + /* checkout the alternate_1 branch */ + checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE; + + cl_git_pass(git_reference_lookup(&branch_reference, g_repo, "refs/heads/alternate_1")); + cl_git_pass(git_reference_peel(&branch_commit, branch_reference, GIT_OBJ_COMMIT)); + cl_git_pass(git_checkout_tree(g_repo, branch_commit, &checkout_options)); + cl_git_pass(git_repository_set_head(g_repo, git_reference_name(branch_reference), NULL, NULL)); + + /* + * Verify state after checkout of parent repository. The submodule ID in the + * HEAD commit and index should be updated, but not the workdir. + */ + + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS_WD_MODIFIED); + + cl_assert(git_oid_streq(git_submodule_head_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + + /* + * Update the submodule and verify the state. + * Now, the HEAD, index, and Workdir commits should all be updated to + * the new commit. + */ + cl_git_pass(git_submodule_update(sm, 0, &update_options)); + cl_assert(git_oid_streq(git_submodule_head_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + + /* verify that the expected callbacks have been called. */ + cl_assert_equal_i(1, update_payload.checkout_progress_called); + + git_submodule_free(sm); + git_object_free(branch_commit); + git_reference_free(branch_reference); +} + +void test_submodule_update__update_blocks_on_dirty_wd(void) +{ + git_submodule *sm = NULL; + git_checkout_options checkout_options = GIT_CHECKOUT_OPTIONS_INIT; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + unsigned int submodule_status = 0; + git_reference *branch_reference = NULL; + git_object *branch_commit = NULL; + struct update_submodule_cb_payload update_payload = { 0 }; + + g_repo = setup_fixture_submodule_simple(); + + update_options.checkout_opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT; + update_options.checkout_opts.notify_cb = checkout_notify_cb; + update_options.checkout_opts.notify_payload = &update_payload; + + /* Initialize and update the sub repository */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_WD_UNINITIALIZED); + + cl_git_pass(git_submodule_update(sm, 1, &update_options)); + + /* verify expected state */ + cl_assert(git_oid_streq(git_submodule_head_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + + /* checkout the alternate_1 branch */ + checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE; + + cl_git_pass(git_reference_lookup(&branch_reference, g_repo, "refs/heads/alternate_1")); + cl_git_pass(git_reference_peel(&branch_commit, branch_reference, GIT_OBJ_COMMIT)); + cl_git_pass(git_checkout_tree(g_repo, branch_commit, &checkout_options)); + cl_git_pass(git_repository_set_head(g_repo, git_reference_name(branch_reference), NULL, NULL)); + + /* + * Verify state after checkout of parent repository. The submodule ID in the + * HEAD commit and index should be updated, but not the workdir. + */ + + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS_WD_MODIFIED); + + cl_assert(git_oid_streq(git_submodule_head_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + + /* + * Create a conflicting edit in the subrepository to verify that + * the submodule update action is blocked. + */ + cl_git_write2file("submodule_simple/testrepo/branch_file.txt", "a conflicting edit", 0, + O_WRONLY | O_CREAT | O_TRUNC, 0755); + + cl_git_fail(git_submodule_update(sm, 0, &update_options)); + + /* verify that the expected callbacks have been called. */ + cl_assert_equal_i(1, update_payload.checkout_notify_called); + + /* verify that the submodule state has not changed. */ + cl_assert(git_oid_streq(git_submodule_head_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + + git_submodule_free(sm); + git_object_free(branch_commit); + git_reference_free(branch_reference); +} + +void test_submodule_update__can_force_update(void) +{ + git_submodule *sm = NULL; + git_checkout_options checkout_options = GIT_CHECKOUT_OPTIONS_INIT; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + unsigned int submodule_status = 0; + git_reference *branch_reference = NULL; + git_object *branch_commit = NULL; + + g_repo = setup_fixture_submodule_simple(); + + /* Initialize and update the sub repository */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_WD_UNINITIALIZED); + + cl_git_pass(git_submodule_update(sm, 1, &update_options)); + + /* verify expected state */ + cl_assert(git_oid_streq(git_submodule_head_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + + /* checkout the alternate_1 branch */ + checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE; + + cl_git_pass(git_reference_lookup(&branch_reference, g_repo, "refs/heads/alternate_1")); + cl_git_pass(git_reference_peel(&branch_commit, branch_reference, GIT_OBJ_COMMIT)); + cl_git_pass(git_checkout_tree(g_repo, branch_commit, &checkout_options)); + cl_git_pass(git_repository_set_head(g_repo, git_reference_name(branch_reference), NULL, NULL)); + + /* + * Verify state after checkout of parent repository. The submodule ID in the + * HEAD commit and index should be updated, but not the workdir. + */ + cl_git_pass(git_submodule_status(&submodule_status, sm)); + cl_assert_equal_i(submodule_status, GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | + GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS_WD_MODIFIED); + + cl_assert(git_oid_streq(git_submodule_head_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + + /* + * Create a conflicting edit in the subrepository to verify that + * the submodule update action is blocked. + */ + cl_git_write2file("submodule_simple/testrepo/branch_file.txt", "a conflicting edit", 0, + O_WRONLY | O_CREAT | O_TRUNC, 0777); + + /* forcefully checkout and verify the submodule state was updated. */ + update_options.checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_pass(git_submodule_update(sm, 0, &update_options)); + cl_assert(git_oid_streq(git_submodule_head_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_wd_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + cl_assert(git_oid_streq(git_submodule_index_id(sm), "a65fedf39aefe402d3bb6e24df4d4f5fe4547750") == 0); + + git_submodule_free(sm); + git_object_free(branch_commit); + git_reference_free(branch_reference); +}