| /* |
| * Copyright (C) the libgit2 contributors. All rights reserved. |
| * |
| * This file is part of libgit2, distributed under the GNU GPL v2 with |
| * a Linking Exception. For full terms see the included COPYING file. |
| */ |
| |
| #include "common.h" |
| #include "git2/config.h" |
| #include "git2/sys/config.h" |
| #include "git2/types.h" |
| #include "git2/repository.h" |
| #include "git2/index.h" |
| #include "git2/submodule.h" |
| #include "buffer.h" |
| #include "buf_text.h" |
| #include "vector.h" |
| #include "posix.h" |
| #include "config_file.h" |
| #include "config.h" |
| #include "repository.h" |
| #include "submodule.h" |
| #include "tree.h" |
| #include "iterator.h" |
| #include "path.h" |
| #include "index.h" |
| |
| #define GIT_MODULES_FILE ".gitmodules" |
| |
| static git_cvar_map _sm_update_map[] = { |
| {GIT_CVAR_STRING, "checkout", GIT_SUBMODULE_UPDATE_CHECKOUT}, |
| {GIT_CVAR_STRING, "rebase", GIT_SUBMODULE_UPDATE_REBASE}, |
| {GIT_CVAR_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE}, |
| {GIT_CVAR_STRING, "none", GIT_SUBMODULE_UPDATE_NONE}, |
| }; |
| |
| static git_cvar_map _sm_ignore_map[] = { |
| {GIT_CVAR_STRING, "none", GIT_SUBMODULE_IGNORE_NONE}, |
| {GIT_CVAR_STRING, "untracked", GIT_SUBMODULE_IGNORE_UNTRACKED}, |
| {GIT_CVAR_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, |
| {GIT_CVAR_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, |
| }; |
| |
| static kh_inline khint_t str_hash_no_trailing_slash(const char *s) |
| { |
| khint_t h; |
| |
| for (h = 0; *s; ++s) |
| if (s[1] != '\0' || *s != '/') |
| h = (h << 5) - h + *s; |
| |
| return h; |
| } |
| |
| static kh_inline int str_equal_no_trailing_slash(const char *a, const char *b) |
| { |
| size_t alen = a ? strlen(a) : 0; |
| size_t blen = b ? strlen(b) : 0; |
| |
| if (alen > 0 && a[alen - 1] == '/') |
| alen--; |
| if (blen > 0 && b[blen - 1] == '/') |
| blen--; |
| |
| return (alen == blen && strncmp(a, b, alen) == 0); |
| } |
| |
| __KHASH_IMPL( |
| str, static kh_inline, const char *, void *, 1, |
| str_hash_no_trailing_slash, str_equal_no_trailing_slash); |
| |
| static int load_submodule_config(git_repository *repo); |
| static git_config_backend *open_gitmodules(git_repository *, bool, const git_oid *); |
| static int lookup_head_remote(git_buf *url, git_repository *repo); |
| static int submodule_get(git_submodule **, git_repository *, const char *, const char *); |
| static void submodule_release(git_submodule *sm, int decr); |
| static int submodule_load_from_index(git_repository *, const git_index_entry *); |
| static int submodule_load_from_head(git_repository*, const char*, const git_oid*); |
| static int submodule_load_from_config(const git_config_entry *, void *); |
| static int submodule_load_from_wd_lite(git_submodule *, const char *, void *); |
| static int submodule_update_config(git_submodule *, const char *, const char *, bool, bool); |
| static void submodule_mode_mismatch(git_repository *, const char *, unsigned int); |
| static int submodule_index_status(unsigned int *status, git_submodule *sm); |
| static int submodule_wd_status(unsigned int *status, git_submodule *sm); |
| |
| static int submodule_cmp(const void *a, const void *b) |
| { |
| return strcmp(((git_submodule *)a)->name, ((git_submodule *)b)->name); |
| } |
| |
| static int submodule_config_key_trunc_puts(git_buf *key, const char *suffix) |
| { |
| ssize_t idx = git_buf_rfind(key, '.'); |
| git_buf_truncate(key, (size_t)(idx + 1)); |
| return git_buf_puts(key, suffix); |
| } |
| |
| /* |
| * PUBLIC APIS |
| */ |
| |
| int git_submodule_lookup( |
| git_submodule **sm_ptr, /* NULL if user only wants to test existence */ |
| git_repository *repo, |
| const char *name) /* trailing slash is allowed */ |
| { |
| int error; |
| khiter_t pos; |
| |
| assert(repo && name); |
| |
| if ((error = load_submodule_config(repo)) < 0) |
| return error; |
| |
| pos = git_strmap_lookup_index(repo->submodules, name); |
| |
| if (!git_strmap_valid_index(repo->submodules, pos)) { |
| error = GIT_ENOTFOUND; |
| |
| /* check if a plausible submodule exists at path */ |
| if (git_repository_workdir(repo)) { |
| git_buf path = GIT_BUF_INIT; |
| |
| if (git_buf_joinpath(&path, git_repository_workdir(repo), name) < 0) |
| return -1; |
| |
| if (git_path_contains_dir(&path, DOT_GIT)) |
| error = GIT_EEXISTS; |
| |
| git_buf_free(&path); |
| } |
| |
| giterr_set(GITERR_SUBMODULE, (error == GIT_ENOTFOUND) ? |
| "No submodule named '%s'" : |
| "Submodule '%s' has not been added yet", name); |
| |
| return error; |
| } |
| |
| if (sm_ptr) |
| *sm_ptr = git_strmap_value_at(repo->submodules, pos); |
| |
| return 0; |
| } |
| |
| int git_submodule_foreach( |
| git_repository *repo, |
| int (*callback)(git_submodule *sm, const char *name, void *payload), |
| void *payload) |
| { |
| int error; |
| git_submodule *sm; |
| git_vector seen = GIT_VECTOR_INIT; |
| git_vector_set_cmp(&seen, submodule_cmp); |
| |
| assert(repo && callback); |
| |
| if ((error = load_submodule_config(repo)) < 0) |
| return error; |
| |
| git_strmap_foreach_value(repo->submodules, sm, { |
| /* Usually the following will not come into play - it just prevents |
| * us from issuing a callback twice for a submodule where the name |
| * and path are not the same. |
| */ |
| if (sm->refcount > 1) { |
| if (git_vector_bsearch(NULL, &seen, sm) != GIT_ENOTFOUND) |
| continue; |
| if ((error = git_vector_insert(&seen, sm)) < 0) |
| break; |
| } |
| |
| if (callback(sm, sm->name, payload)) { |
| giterr_clear(); |
| error = GIT_EUSER; |
| break; |
| } |
| }); |
| |
| git_vector_free(&seen); |
| |
| return error; |
| } |
| |
| void git_submodule_config_free(git_repository *repo) |
| { |
| git_strmap *smcfg; |
| git_submodule *sm; |
| |
| assert(repo); |
| |
| smcfg = repo->submodules; |
| repo->submodules = NULL; |
| |
| if (smcfg == NULL) |
| return; |
| |
| git_strmap_foreach_value(smcfg, sm, { |
| submodule_release(sm,1); |
| }); |
| git_strmap_free(smcfg); |
| } |
| |
| int git_submodule_add_setup( |
| git_submodule **submodule, |
| git_repository *repo, |
| const char *url, |
| const char *path, |
| int use_gitlink) |
| { |
| int error = 0; |
| git_config_backend *mods = NULL; |
| git_submodule *sm; |
| git_buf name = GIT_BUF_INIT, real_url = GIT_BUF_INIT; |
| git_repository_init_options initopt = GIT_REPOSITORY_INIT_OPTIONS_INIT; |
| git_repository *subrepo = NULL; |
| |
| assert(repo && url && path); |
| |
| /* see if there is already an entry for this submodule */ |
| |
| if (git_submodule_lookup(&sm, repo, path) < 0) |
| giterr_clear(); |
| else { |
| giterr_set(GITERR_SUBMODULE, |
| "Attempt to add a submodule that already exists"); |
| return GIT_EEXISTS; |
| } |
| |
| /* resolve parameters */ |
| |
| if (url[0] == '.' && (url[1] == '/' || (url[1] == '.' && url[2] == '/'))) { |
| if (!(error = lookup_head_remote(&real_url, repo))) |
| error = git_path_apply_relative(&real_url, url); |
| } else if (strchr(url, ':') != NULL || url[0] == '/') { |
| error = git_buf_sets(&real_url, url); |
| } else { |
| giterr_set(GITERR_SUBMODULE, "Invalid format for submodule URL"); |
| error = -1; |
| } |
| if (error) |
| goto cleanup; |
| |
| /* validate and normalize path */ |
| |
| if (git__prefixcmp(path, git_repository_workdir(repo)) == 0) |
| path += strlen(git_repository_workdir(repo)); |
| |
| if (git_path_root(path) >= 0) { |
| giterr_set(GITERR_SUBMODULE, "Submodule path must be a relative path"); |
| error = -1; |
| goto cleanup; |
| } |
| |
| /* update .gitmodules */ |
| |
| if ((mods = open_gitmodules(repo, true, NULL)) == NULL) { |
| giterr_set(GITERR_SUBMODULE, |
| "Adding submodules to a bare repository is not supported (for now)"); |
| return -1; |
| } |
| |
| if ((error = git_buf_printf(&name, "submodule.%s.path", path)) < 0 || |
| (error = git_config_file_set_string(mods, name.ptr, path)) < 0) |
| goto cleanup; |
| |
| if ((error = submodule_config_key_trunc_puts(&name, "url")) < 0 || |
| (error = git_config_file_set_string(mods, name.ptr, real_url.ptr)) < 0) |
| goto cleanup; |
| |
| git_buf_clear(&name); |
| |
| /* init submodule repository and add origin remote as needed */ |
| |
| error = git_buf_joinpath(&name, git_repository_workdir(repo), path); |
| if (error < 0) |
| goto cleanup; |
| |
| /* New style: sub-repo goes in <repo-dir>/modules/<name>/ with a |
| * gitlink in the sub-repo workdir directory to that repository |
| * |
| * Old style: sub-repo goes directly into repo/<name>/.git/ |
| */ |
| |
| initopt.flags = GIT_REPOSITORY_INIT_MKPATH | |
| GIT_REPOSITORY_INIT_NO_REINIT; |
| initopt.origin_url = real_url.ptr; |
| |
| if (git_path_exists(name.ptr) && |
| git_path_contains(&name, DOT_GIT)) |
| { |
| /* repo appears to already exist - reinit? */ |
| } |
| else if (use_gitlink) { |
| git_buf repodir = GIT_BUF_INIT; |
| |
| error = git_buf_join_n( |
| &repodir, '/', 3, git_repository_path(repo), "modules", path); |
| if (error < 0) |
| goto cleanup; |
| |
| initopt.workdir_path = name.ptr; |
| initopt.flags |= GIT_REPOSITORY_INIT_NO_DOTGIT_DIR; |
| |
| error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); |
| |
| git_buf_free(&repodir); |
| } |
| else { |
| error = git_repository_init_ext(&subrepo, name.ptr, &initopt); |
| } |
| if (error < 0) |
| goto cleanup; |
| |
| /* add submodule to hash and "reload" it */ |
| |
| if (!(error = submodule_get(&sm, repo, path, NULL)) && |
| !(error = git_submodule_reload(sm))) |
| error = git_submodule_init(sm, false); |
| |
| cleanup: |
| if (submodule != NULL) |
| *submodule = !error ? sm : NULL; |
| |
| if (mods != NULL) |
| git_config_file_free(mods); |
| git_repository_free(subrepo); |
| git_buf_free(&real_url); |
| git_buf_free(&name); |
| |
| return error; |
| } |
| |
| int git_submodule_add_finalize(git_submodule *sm) |
| { |
| int error; |
| git_index *index; |
| |
| assert(sm); |
| |
| if ((error = git_repository_index__weakptr(&index, sm->owner)) < 0 || |
| (error = git_index_add_bypath(index, GIT_MODULES_FILE)) < 0) |
| return error; |
| |
| return git_submodule_add_to_index(sm, true); |
| } |
| |
| int git_submodule_add_to_index(git_submodule *sm, int write_index) |
| { |
| int error; |
| git_repository *repo, *sm_repo = NULL; |
| git_index *index; |
| git_buf path = GIT_BUF_INIT; |
| git_commit *head; |
| git_index_entry entry; |
| struct stat st; |
| |
| assert(sm); |
| |
| repo = sm->owner; |
| |
| /* force reload of wd OID by git_submodule_open */ |
| sm->flags = sm->flags & ~GIT_SUBMODULE_STATUS__WD_OID_VALID; |
| |
| if ((error = git_repository_index__weakptr(&index, repo)) < 0 || |
| (error = git_buf_joinpath( |
| &path, git_repository_workdir(repo), sm->path)) < 0 || |
| (error = git_submodule_open(&sm_repo, sm)) < 0) |
| goto cleanup; |
| |
| /* read stat information for submodule working directory */ |
| if (p_stat(path.ptr, &st) < 0) { |
| giterr_set(GITERR_SUBMODULE, |
| "Cannot add submodule without working directory"); |
| error = -1; |
| goto cleanup; |
| } |
| |
| memset(&entry, 0, sizeof(entry)); |
| entry.path = sm->path; |
| git_index_entry__init_from_stat(&entry, &st); |
| |
| /* calling git_submodule_open will have set sm->wd_oid if possible */ |
| if ((sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) == 0) { |
| giterr_set(GITERR_SUBMODULE, |
| "Cannot add submodule without HEAD to index"); |
| error = -1; |
| goto cleanup; |
| } |
| git_oid_cpy(&entry.oid, &sm->wd_oid); |
| |
| if ((error = git_commit_lookup(&head, sm_repo, &sm->wd_oid)) < 0) |
| goto cleanup; |
| |
| entry.ctime.seconds = git_commit_time(head); |
| entry.ctime.nanoseconds = 0; |
| entry.mtime.seconds = git_commit_time(head); |
| entry.mtime.nanoseconds = 0; |
| |
| git_commit_free(head); |
| |
| /* add it */ |
| error = git_index_add(index, &entry); |
| |
| /* write it, if requested */ |
| if (!error && write_index) { |
| error = git_index_write(index); |
| |
| if (!error) |
| git_oid_cpy(&sm->index_oid, &sm->wd_oid); |
| } |
| |
| cleanup: |
| git_repository_free(sm_repo); |
| git_buf_free(&path); |
| return error; |
| } |
| |
| int git_submodule_save(git_submodule *submodule) |
| { |
| int error = 0; |
| git_config_backend *mods; |
| git_buf key = GIT_BUF_INIT; |
| |
| assert(submodule); |
| |
| mods = open_gitmodules(submodule->owner, true, NULL); |
| if (!mods) { |
| giterr_set(GITERR_SUBMODULE, |
| "Adding submodules to a bare repository is not supported (for now)"); |
| return -1; |
| } |
| |
| if ((error = git_buf_printf(&key, "submodule.%s.", submodule->name)) < 0) |
| goto cleanup; |
| |
| /* save values for path, url, update, ignore, fetchRecurseSubmodules */ |
| |
| if ((error = submodule_config_key_trunc_puts(&key, "path")) < 0 || |
| (error = git_config_file_set_string(mods, key.ptr, submodule->path)) < 0) |
| goto cleanup; |
| |
| if ((error = submodule_config_key_trunc_puts(&key, "url")) < 0 || |
| (error = git_config_file_set_string(mods, key.ptr, submodule->url)) < 0) |
| goto cleanup; |
| |
| if (!(error = submodule_config_key_trunc_puts(&key, "update")) && |
| submodule->update != GIT_SUBMODULE_UPDATE_DEFAULT) |
| { |
| const char *val = (submodule->update == GIT_SUBMODULE_UPDATE_CHECKOUT) ? |
| NULL : _sm_update_map[submodule->update].str_match; |
| error = git_config_file_set_string(mods, key.ptr, val); |
| } |
| if (error < 0) |
| goto cleanup; |
| |
| if (!(error = submodule_config_key_trunc_puts(&key, "ignore")) && |
| submodule->ignore != GIT_SUBMODULE_IGNORE_DEFAULT) |
| { |
| const char *val = (submodule->ignore == GIT_SUBMODULE_IGNORE_NONE) ? |
| NULL : _sm_ignore_map[submodule->ignore].str_match; |
| error = git_config_file_set_string(mods, key.ptr, val); |
| } |
| if (error < 0) |
| goto cleanup; |
| |
| if ((error = submodule_config_key_trunc_puts( |
| &key, "fetchRecurseSubmodules")) < 0 || |
| (error = git_config_file_set_string( |
| mods, key.ptr, submodule->fetch_recurse ? "true" : "false")) < 0) |
| goto cleanup; |
| |
| /* update internal defaults */ |
| |
| submodule->ignore_default = submodule->ignore; |
| submodule->update_default = submodule->update; |
| submodule->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; |
| |
| cleanup: |
| if (mods != NULL) |
| git_config_file_free(mods); |
| git_buf_free(&key); |
| |
| return error; |
| } |
| |
| git_repository *git_submodule_owner(git_submodule *submodule) |
| { |
| assert(submodule); |
| return submodule->owner; |
| } |
| |
| const char *git_submodule_name(git_submodule *submodule) |
| { |
| assert(submodule); |
| return submodule->name; |
| } |
| |
| const char *git_submodule_path(git_submodule *submodule) |
| { |
| assert(submodule); |
| return submodule->path; |
| } |
| |
| const char *git_submodule_url(git_submodule *submodule) |
| { |
| assert(submodule); |
| return submodule->url; |
| } |
| |
| int git_submodule_set_url(git_submodule *submodule, const char *url) |
| { |
| assert(submodule && url); |
| |
| git__free(submodule->url); |
| |
| submodule->url = git__strdup(url); |
| GITERR_CHECK_ALLOC(submodule->url); |
| |
| return 0; |
| } |
| |
| const git_oid *git_submodule_index_id(git_submodule *submodule) |
| { |
| assert(submodule); |
| |
| if (submodule->flags & GIT_SUBMODULE_STATUS__INDEX_OID_VALID) |
| return &submodule->index_oid; |
| else |
| return NULL; |
| } |
| |
| const git_oid *git_submodule_head_id(git_submodule *submodule) |
| { |
| assert(submodule); |
| |
| if (submodule->flags & GIT_SUBMODULE_STATUS__HEAD_OID_VALID) |
| return &submodule->head_oid; |
| else |
| return NULL; |
| } |
| |
| const git_oid *git_submodule_wd_id(git_submodule *submodule) |
| { |
| assert(submodule); |
| |
| if (!(submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID)) { |
| git_repository *subrepo; |
| |
| /* calling submodule open grabs the HEAD OID if possible */ |
| if (!git_submodule_open(&subrepo, submodule)) |
| git_repository_free(subrepo); |
| else |
| giterr_clear(); |
| } |
| |
| if (submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) |
| return &submodule->wd_oid; |
| else |
| return NULL; |
| } |
| |
| git_submodule_ignore_t git_submodule_ignore(git_submodule *submodule) |
| { |
| assert(submodule); |
| return submodule->ignore; |
| } |
| |
| git_submodule_ignore_t git_submodule_set_ignore( |
| git_submodule *submodule, git_submodule_ignore_t ignore) |
| { |
| git_submodule_ignore_t old; |
| |
| assert(submodule); |
| |
| if (ignore == GIT_SUBMODULE_IGNORE_DEFAULT) |
| ignore = submodule->ignore_default; |
| |
| old = submodule->ignore; |
| submodule->ignore = ignore; |
| return old; |
| } |
| |
| git_submodule_update_t git_submodule_update(git_submodule *submodule) |
| { |
| assert(submodule); |
| return submodule->update; |
| } |
| |
| git_submodule_update_t git_submodule_set_update( |
| git_submodule *submodule, git_submodule_update_t update) |
| { |
| git_submodule_update_t old; |
| |
| assert(submodule); |
| |
| if (update == GIT_SUBMODULE_UPDATE_DEFAULT) |
| update = submodule->update_default; |
| |
| old = submodule->update; |
| submodule->update = update; |
| return old; |
| } |
| |
| int git_submodule_fetch_recurse_submodules( |
| git_submodule *submodule) |
| { |
| assert(submodule); |
| return submodule->fetch_recurse; |
| } |
| |
| int git_submodule_set_fetch_recurse_submodules( |
| git_submodule *submodule, |
| int fetch_recurse_submodules) |
| { |
| int old; |
| |
| assert(submodule); |
| |
| old = submodule->fetch_recurse; |
| submodule->fetch_recurse = (fetch_recurse_submodules != 0); |
| return old; |
| } |
| |
| int git_submodule_init(git_submodule *submodule, int overwrite) |
| { |
| int error; |
| |
| /* write "submodule.NAME.url" */ |
| |
| if (!submodule->url) { |
| giterr_set(GITERR_SUBMODULE, |
| "No URL configured for submodule '%s'", submodule->name); |
| return -1; |
| } |
| |
| error = submodule_update_config( |
| submodule, "url", submodule->url, overwrite != 0, false); |
| if (error < 0) |
| return error; |
| |
| /* write "submodule.NAME.update" if not default */ |
| |
| if (submodule->update == GIT_SUBMODULE_UPDATE_CHECKOUT) |
| error = submodule_update_config( |
| submodule, "update", NULL, (overwrite != 0), false); |
| else if (submodule->update != GIT_SUBMODULE_UPDATE_DEFAULT) |
| error = submodule_update_config( |
| submodule, "update", |
| _sm_update_map[submodule->update].str_match, |
| (overwrite != 0), false); |
| |
| return error; |
| } |
| |
| int git_submodule_sync(git_submodule *submodule) |
| { |
| if (!submodule->url) { |
| giterr_set(GITERR_SUBMODULE, |
| "No URL configured for submodule '%s'", submodule->name); |
| return -1; |
| } |
| |
| /* copy URL over to config only if it already exists */ |
| |
| return submodule_update_config( |
| submodule, "url", submodule->url, true, true); |
| } |
| |
| int git_submodule_open( |
| git_repository **subrepo, |
| git_submodule *submodule) |
| { |
| int error; |
| git_buf path = GIT_BUF_INIT; |
| git_repository *repo; |
| const char *workdir; |
| |
| assert(submodule && subrepo); |
| |
| repo = submodule->owner; |
| workdir = git_repository_workdir(repo); |
| |
| if (!workdir) { |
| giterr_set(GITERR_REPOSITORY, |
| "Cannot open submodule repository in a bare repo"); |
| return GIT_ENOTFOUND; |
| } |
| |
| if ((submodule->flags & GIT_SUBMODULE_STATUS_IN_WD) == 0) { |
| giterr_set(GITERR_REPOSITORY, |
| "Cannot open submodule repository that is not checked out"); |
| return GIT_ENOTFOUND; |
| } |
| |
| if (git_buf_joinpath(&path, workdir, submodule->path) < 0) |
| return -1; |
| |
| error = git_repository_open(subrepo, path.ptr); |
| |
| git_buf_free(&path); |
| |
| /* if we have opened the submodule successfully, let's grab the HEAD OID */ |
| if (!error) { |
| if (!git_reference_name_to_id( |
| &submodule->wd_oid, *subrepo, GIT_HEAD_FILE)) |
| submodule->flags |= GIT_SUBMODULE_STATUS__WD_OID_VALID; |
| else |
| giterr_clear(); |
| } |
| |
| return error; |
| } |
| |
| int git_submodule_reload_all(git_repository *repo) |
| { |
| assert(repo); |
| git_submodule_config_free(repo); |
| return load_submodule_config(repo); |
| } |
| |
| int git_submodule_reload(git_submodule *submodule) |
| { |
| git_repository *repo; |
| git_index *index; |
| int error; |
| size_t pos; |
| git_tree *head; |
| git_config_backend *mods; |
| |
| assert(submodule); |
| |
| /* refresh index data */ |
| |
| repo = submodule->owner; |
| if (git_repository_index__weakptr(&index, repo) < 0) |
| return -1; |
| |
| submodule->flags = submodule->flags & |
| ~(GIT_SUBMODULE_STATUS_IN_INDEX | |
| GIT_SUBMODULE_STATUS__INDEX_OID_VALID); |
| |
| if (!git_index_find(&pos, index, submodule->path)) { |
| const git_index_entry *entry = git_index_get_byindex(index, pos); |
| |
| if (S_ISGITLINK(entry->mode)) { |
| if ((error = submodule_load_from_index(repo, entry)) < 0) |
| return error; |
| } else { |
| submodule_mode_mismatch( |
| repo, entry->path, GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE); |
| } |
| } |
| |
| /* refresh HEAD tree data */ |
| |
| if (!(error = git_repository_head_tree(&head, repo))) { |
| git_tree_entry *te; |
| |
| submodule->flags = submodule->flags & |
| ~(GIT_SUBMODULE_STATUS_IN_HEAD | |
| GIT_SUBMODULE_STATUS__HEAD_OID_VALID); |
| |
| if (!(error = git_tree_entry_bypath(&te, head, submodule->path))) { |
| |
| if (S_ISGITLINK(te->attr)) { |
| error = submodule_load_from_head(repo, submodule->path, &te->oid); |
| } else { |
| submodule_mode_mismatch( |
| repo, submodule->path, |
| GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE); |
| } |
| |
| git_tree_entry_free(te); |
| } |
| else if (error == GIT_ENOTFOUND) { |
| giterr_clear(); |
| error = 0; |
| } |
| |
| git_tree_free(head); |
| } |
| |
| if (error < 0) |
| return error; |
| |
| /* refresh config data */ |
| |
| if ((mods = open_gitmodules(repo, false, NULL)) != NULL) { |
| git_buf path = GIT_BUF_INIT; |
| |
| git_buf_sets(&path, "submodule\\."); |
| git_buf_text_puts_escape_regex(&path, submodule->name); |
| git_buf_puts(&path, ".*"); |
| |
| if (git_buf_oom(&path)) |
| error = -1; |
| else |
| error = git_config_file_foreach_match( |
| mods, path.ptr, submodule_load_from_config, repo); |
| |
| git_buf_free(&path); |
| git_config_file_free(mods); |
| } |
| |
| if (error < 0) |
| return error; |
| |
| /* refresh wd data */ |
| |
| submodule->flags = submodule->flags & |
| ~(GIT_SUBMODULE_STATUS_IN_WD | GIT_SUBMODULE_STATUS__WD_OID_VALID); |
| |
| error = submodule_load_from_wd_lite(submodule, submodule->path, NULL); |
| |
| return error; |
| } |
| |
| int git_submodule_status( |
| unsigned int *status, |
| git_submodule *submodule) |
| { |
| int error = 0; |
| unsigned int status_val; |
| |
| assert(status && submodule); |
| |
| status_val = GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(submodule->flags); |
| |
| if (submodule->ignore != GIT_SUBMODULE_IGNORE_ALL) { |
| if (!(error = submodule_index_status(&status_val, submodule))) |
| error = submodule_wd_status(&status_val, submodule); |
| } |
| |
| *status = status_val; |
| |
| return error; |
| } |
| |
| int git_submodule_location( |
| unsigned int *location_status, |
| git_submodule *submodule) |
| { |
| assert(location_status && submodule); |
| |
| *location_status = submodule->flags & |
| (GIT_SUBMODULE_STATUS_IN_HEAD | GIT_SUBMODULE_STATUS_IN_INDEX | |
| GIT_SUBMODULE_STATUS_IN_CONFIG | GIT_SUBMODULE_STATUS_IN_WD); |
| |
| return 0; |
| } |
| |
| |
| /* |
| * INTERNAL FUNCTIONS |
| */ |
| |
| static git_submodule *submodule_alloc(git_repository *repo, const char *name) |
| { |
| git_submodule *sm; |
| |
| if (!name || !strlen(name)) { |
| giterr_set(GITERR_SUBMODULE, "Invalid submodule name"); |
| return NULL; |
| } |
| |
| sm = git__calloc(1, sizeof(git_submodule)); |
| if (sm == NULL) |
| goto fail; |
| |
| sm->path = sm->name = git__strdup(name); |
| if (!sm->name) |
| goto fail; |
| |
| sm->owner = repo; |
| sm->refcount = 1; |
| |
| return sm; |
| |
| fail: |
| submodule_release(sm, 0); |
| return NULL; |
| } |
| |
| static void submodule_release(git_submodule *sm, int decr) |
| { |
| if (!sm) |
| return; |
| |
| sm->refcount -= decr; |
| |
| if (sm->refcount == 0) { |
| if (sm->name != sm->path) { |
| git__free(sm->path); |
| sm->path = NULL; |
| } |
| |
| git__free(sm->name); |
| sm->name = NULL; |
| |
| git__free(sm->url); |
| sm->url = NULL; |
| |
| sm->owner = NULL; |
| |
| git__free(sm); |
| } |
| } |
| |
| static int submodule_get( |
| git_submodule **sm_ptr, |
| git_repository *repo, |
| const char *name, |
| const char *alternate) |
| { |
| git_strmap *smcfg = repo->submodules; |
| khiter_t pos; |
| git_submodule *sm; |
| int error; |
| |
| assert(repo && name); |
| |
| pos = git_strmap_lookup_index(smcfg, name); |
| |
| if (!git_strmap_valid_index(smcfg, pos) && alternate) |
| pos = git_strmap_lookup_index(smcfg, alternate); |
| |
| if (!git_strmap_valid_index(smcfg, pos)) { |
| sm = submodule_alloc(repo, name); |
| |
| /* insert value at name - if another thread beats us to it, then use |
| * their record and release our own. |
| */ |
| pos = kh_put(str, smcfg, sm->name, &error); |
| |
| if (error < 0) { |
| submodule_release(sm, 1); |
| sm = NULL; |
| } else if (error == 0) { |
| submodule_release(sm, 1); |
| sm = git_strmap_value_at(smcfg, pos); |
| } else { |
| git_strmap_set_value_at(smcfg, pos, sm); |
| } |
| } else { |
| sm = git_strmap_value_at(smcfg, pos); |
| } |
| |
| *sm_ptr = sm; |
| |
| return (sm != NULL) ? 0 : -1; |
| } |
| |
| static int submodule_load_from_index( |
| git_repository *repo, const git_index_entry *entry) |
| { |
| git_submodule *sm; |
| |
| if (submodule_get(&sm, repo, entry->path, NULL) < 0) |
| return -1; |
| |
| if (sm->flags & GIT_SUBMODULE_STATUS_IN_INDEX) { |
| sm->flags |= GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES; |
| return 0; |
| } |
| |
| sm->flags |= GIT_SUBMODULE_STATUS_IN_INDEX; |
| |
| git_oid_cpy(&sm->index_oid, &entry->oid); |
| sm->flags |= GIT_SUBMODULE_STATUS__INDEX_OID_VALID; |
| |
| return 0; |
| } |
| |
| static int submodule_load_from_head( |
| git_repository *repo, const char *path, const git_oid *oid) |
| { |
| git_submodule *sm; |
| |
| if (submodule_get(&sm, repo, path, NULL) < 0) |
| return -1; |
| |
| sm->flags |= GIT_SUBMODULE_STATUS_IN_HEAD; |
| |
| git_oid_cpy(&sm->head_oid, oid); |
| sm->flags |= GIT_SUBMODULE_STATUS__HEAD_OID_VALID; |
| |
| return 0; |
| } |
| |
| static int submodule_config_error(const char *property, const char *value) |
| { |
| giterr_set(GITERR_INVALID, |
| "Invalid value for submodule '%s' property: '%s'", property, value); |
| return -1; |
| } |
| |
| static int submodule_load_from_config( |
| const git_config_entry *entry, void *data) |
| { |
| git_repository *repo = data; |
| git_strmap *smcfg = repo->submodules; |
| const char *namestart, *property, *alternate = NULL; |
| const char *key = entry->name, *value = entry->value; |
| git_buf name = GIT_BUF_INIT; |
| git_submodule *sm; |
| bool is_path; |
| int error = 0; |
| |
| if (git__prefixcmp(key, "submodule.") != 0) |
| return 0; |
| |
| namestart = key + strlen("submodule."); |
| property = strrchr(namestart, '.'); |
| if (property == NULL) |
| return 0; |
| property++; |
| is_path = (strcasecmp(property, "path") == 0); |
| |
| if (git_buf_set(&name, namestart, property - namestart - 1) < 0) |
| return -1; |
| |
| if (submodule_get(&sm, repo, name.ptr, is_path ? value : NULL) < 0) { |
| git_buf_free(&name); |
| return -1; |
| } |
| |
| sm->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; |
| |
| /* Only from config might we get differing names & paths. If so, then |
| * update the submodule and insert under the alternative key. |
| */ |
| |
| /* TODO: if case insensitive filesystem, then the following strcmps |
| * should be strcasecmp |
| */ |
| |
| if (strcmp(sm->name, name.ptr) != 0) { |
| alternate = sm->name = git_buf_detach(&name); |
| } else if (is_path && value && strcmp(sm->path, value) != 0) { |
| alternate = sm->path = git__strdup(value); |
| if (!sm->path) |
| error = -1; |
| } |
| if (alternate) { |
| void *old_sm = NULL; |
| git_strmap_insert2(smcfg, alternate, sm, old_sm, error); |
| |
| if (error >= 0) |
| sm->refcount++; /* inserted under a new key */ |
| |
| /* if we replaced an old module under this key, release the old one */ |
| if (old_sm && ((git_submodule *)old_sm) != sm) { |
| submodule_release(old_sm, 1); |
| /* TODO: log warning about multiple submodules with same path */ |
| } |
| } |
| |
| git_buf_free(&name); |
| if (error < 0) |
| return error; |
| |
| /* TODO: Look up path in index and if it is present but not a GITLINK |
| * then this should be deleted (at least to match git's behavior) |
| */ |
| |
| if (is_path) |
| return 0; |
| |
| /* copy other properties into submodule entry */ |
| if (strcasecmp(property, "url") == 0) { |
| git__free(sm->url); |
| sm->url = NULL; |
| |
| if (value != NULL && (sm->url = git__strdup(value)) == NULL) |
| return -1; |
| } |
| else if (strcasecmp(property, "update") == 0) { |
| int val; |
| if (git_config_lookup_map_value( |
| &val, _sm_update_map, ARRAY_SIZE(_sm_update_map), value) < 0) |
| return submodule_config_error("update", value); |
| sm->update_default = sm->update = (git_submodule_update_t)val; |
| } |
| else if (strcasecmp(property, "fetchRecurseSubmodules") == 0) { |
| if (git__parse_bool(&sm->fetch_recurse, value) < 0) |
| return submodule_config_error("fetchRecurseSubmodules", value); |
| } |
| else if (strcasecmp(property, "ignore") == 0) { |
| int val; |
| if (git_config_lookup_map_value( |
| &val, _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value) < 0) |
| return submodule_config_error("ignore", value); |
| sm->ignore_default = sm->ignore = (git_submodule_ignore_t)val; |
| } |
| /* ignore other unknown submodule properties */ |
| |
| return 0; |
| } |
| |
| static int submodule_load_from_wd_lite( |
| git_submodule *sm, const char *name, void *payload) |
| { |
| git_repository *repo = git_submodule_owner(sm); |
| git_buf path = GIT_BUF_INIT; |
| |
| GIT_UNUSED(name); |
| GIT_UNUSED(payload); |
| |
| if (git_buf_joinpath(&path, git_repository_workdir(repo), sm->path) < 0) |
| return -1; |
| |
| if (git_path_isdir(path.ptr)) |
| sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; |
| |
| if (git_path_contains(&path, DOT_GIT)) |
| sm->flags |= GIT_SUBMODULE_STATUS_IN_WD; |
| |
| git_buf_free(&path); |
| |
| return 0; |
| } |
| |
| static void submodule_mode_mismatch( |
| git_repository *repo, const char *path, unsigned int flag) |
| { |
| khiter_t pos = git_strmap_lookup_index(repo->submodules, path); |
| |
| if (git_strmap_valid_index(repo->submodules, pos)) { |
| git_submodule *sm = git_strmap_value_at(repo->submodules, pos); |
| |
| sm->flags |= flag; |
| } |
| } |
| |
| static int load_submodule_config_from_index( |
| git_repository *repo, git_oid *gitmodules_oid) |
| { |
| int error; |
| git_index *index; |
| git_iterator *i; |
| const git_index_entry *entry; |
| |
| if ((error = git_repository_index__weakptr(&index, repo)) < 0 || |
| (error = git_iterator_for_index(&i, index, 0, NULL, NULL)) < 0) |
| return error; |
| |
| while (!(error = git_iterator_advance(&entry, i))) { |
| |
| if (S_ISGITLINK(entry->mode)) { |
| error = submodule_load_from_index(repo, entry); |
| if (error < 0) |
| break; |
| } else { |
| submodule_mode_mismatch( |
| repo, entry->path, GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE); |
| |
| if (strcmp(entry->path, GIT_MODULES_FILE) == 0) |
| git_oid_cpy(gitmodules_oid, &entry->oid); |
| } |
| } |
| |
| if (error == GIT_ITEROVER) |
| error = 0; |
| |
| git_iterator_free(i); |
| |
| return error; |
| } |
| |
| static int load_submodule_config_from_head( |
| git_repository *repo, git_oid *gitmodules_oid) |
| { |
| int error; |
| git_tree *head; |
| git_iterator *i; |
| const git_index_entry *entry; |
| |
| if ((error = git_repository_head_tree(&head, repo)) < 0) |
| return error; |
| |
| if ((error = git_iterator_for_tree(&i, head, 0, NULL, NULL)) < 0) { |
| git_tree_free(head); |
| return error; |
| } |
| |
| while (!(error = git_iterator_advance(&entry, i))) { |
| |
| if (S_ISGITLINK(entry->mode)) { |
| error = submodule_load_from_head(repo, entry->path, &entry->oid); |
| if (error < 0) |
| break; |
| } else { |
| submodule_mode_mismatch( |
| repo, entry->path, GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE); |
| |
| if (strcmp(entry->path, GIT_MODULES_FILE) == 0 && |
| git_oid_iszero(gitmodules_oid)) |
| git_oid_cpy(gitmodules_oid, &entry->oid); |
| } |
| } |
| |
| if (error == GIT_ITEROVER) |
| error = 0; |
| |
| git_iterator_free(i); |
| git_tree_free(head); |
| |
| return error; |
| } |
| |
| static git_config_backend *open_gitmodules( |
| git_repository *repo, |
| bool okay_to_create, |
| const git_oid *gitmodules_oid) |
| { |
| const char *workdir = git_repository_workdir(repo); |
| git_buf path = GIT_BUF_INIT; |
| git_config_backend *mods = NULL; |
| |
| if (workdir != NULL) { |
| if (git_buf_joinpath(&path, workdir, GIT_MODULES_FILE) != 0) |
| return NULL; |
| |
| if (okay_to_create || git_path_isfile(path.ptr)) { |
| /* git_config_file__ondisk should only fail if OOM */ |
| if (git_config_file__ondisk(&mods, path.ptr) < 0) |
| mods = NULL; |
| /* open should only fail here if the file is malformed */ |
| else if (git_config_file_open(mods, GIT_CONFIG_LEVEL_LOCAL) < 0) { |
| git_config_file_free(mods); |
| mods = NULL; |
| } |
| } |
| } |
| |
| if (!mods && gitmodules_oid && !git_oid_iszero(gitmodules_oid)) { |
| /* TODO: Retrieve .gitmodules content from ODB */ |
| |
| /* Should we actually do this? Core git does not, but it means you |
| * can't really get much information about submodules on bare repos. |
| */ |
| } |
| |
| git_buf_free(&path); |
| |
| return mods; |
| } |
| |
| static int load_submodule_config(git_repository *repo) |
| { |
| int error; |
| git_oid gitmodules_oid; |
| git_buf path = GIT_BUF_INIT; |
| git_config_backend *mods = NULL; |
| |
| if (repo->submodules) |
| return 0; |
| |
| memset(&gitmodules_oid, 0, sizeof(gitmodules_oid)); |
| |
| /* Submodule data is kept in a hashtable keyed by both name and path. |
| * These are usually the same, but that is not guaranteed. |
| */ |
| if (!repo->submodules) { |
| repo->submodules = git_strmap_alloc(); |
| GITERR_CHECK_ALLOC(repo->submodules); |
| } |
| |
| /* add submodule information from index */ |
| |
| if ((error = load_submodule_config_from_index(repo, &gitmodules_oid)) < 0) |
| goto cleanup; |
| |
| /* add submodule information from HEAD */ |
| |
| if ((error = load_submodule_config_from_head(repo, &gitmodules_oid)) < 0) |
| goto cleanup; |
| |
| /* add submodule information from .gitmodules */ |
| |
| if ((mods = open_gitmodules(repo, false, &gitmodules_oid)) != NULL) |
| error = git_config_file_foreach(mods, submodule_load_from_config, repo); |
| |
| if (error != 0) |
| goto cleanup; |
| |
| /* shallow scan submodules in work tree */ |
| |
| if (!git_repository_is_bare(repo)) |
| error = git_submodule_foreach(repo, submodule_load_from_wd_lite, NULL); |
| |
| cleanup: |
| git_buf_free(&path); |
| |
| if (mods != NULL) |
| git_config_file_free(mods); |
| |
| if (error) |
| git_submodule_config_free(repo); |
| |
| return error; |
| } |
| |
| static int lookup_head_remote(git_buf *url, git_repository *repo) |
| { |
| int error; |
| git_config *cfg; |
| git_reference *head = NULL, *remote = NULL; |
| const char *tgt, *scan; |
| git_buf key = GIT_BUF_INIT; |
| |
| /* 1. resolve HEAD -> refs/heads/BRANCH |
| * 2. lookup config branch.BRANCH.remote -> ORIGIN |
| * 3. lookup remote.ORIGIN.url |
| */ |
| |
| if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) |
| return error; |
| |
| if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) { |
| giterr_set(GITERR_SUBMODULE, |
| "Cannot resolve relative URL when HEAD cannot be resolved"); |
| error = GIT_ENOTFOUND; |
| goto cleanup; |
| } |
| |
| if (git_reference_type(head) != GIT_REF_SYMBOLIC) { |
| giterr_set(GITERR_SUBMODULE, |
| "Cannot resolve relative URL when HEAD is not symbolic"); |
| error = GIT_ENOTFOUND; |
| goto cleanup; |
| } |
| |
| if ((error = git_branch_upstream(&remote, head)) < 0) |
| goto cleanup; |
| |
| /* remote should refer to something like refs/remotes/ORIGIN/BRANCH */ |
| |
| if (git_reference_type(remote) != GIT_REF_SYMBOLIC || |
| git__prefixcmp(git_reference_symbolic_target(remote), GIT_REFS_REMOTES_DIR) != 0) |
| { |
| giterr_set(GITERR_SUBMODULE, |
| "Cannot resolve relative URL when HEAD is not symbolic"); |
| error = GIT_ENOTFOUND; |
| goto cleanup; |
| } |
| |
| scan = tgt = git_reference_symbolic_target(remote) + strlen(GIT_REFS_REMOTES_DIR); |
| while (*scan && (*scan != '/' || (scan > tgt && scan[-1] != '\\'))) |
| scan++; /* find non-escaped slash to end ORIGIN name */ |
| |
| error = git_buf_printf(&key, "remote.%.*s.url", (int)(scan - tgt), tgt); |
| if (error < 0) |
| goto cleanup; |
| |
| if ((error = git_config_get_string(&tgt, cfg, key.ptr)) < 0) |
| goto cleanup; |
| |
| error = git_buf_sets(url, tgt); |
| |
| cleanup: |
| git_buf_free(&key); |
| git_reference_free(head); |
| git_reference_free(remote); |
| |
| return error; |
| } |
| |
| static int submodule_update_config( |
| git_submodule *submodule, |
| const char *attr, |
| const char *value, |
| bool overwrite, |
| bool only_existing) |
| { |
| int error; |
| git_config *config; |
| git_buf key = GIT_BUF_INIT; |
| const char *old = NULL; |
| |
| assert(submodule); |
| |
| error = git_repository_config__weakptr(&config, submodule->owner); |
| if (error < 0) |
| return error; |
| |
| error = git_buf_printf(&key, "submodule.%s.%s", submodule->name, attr); |
| if (error < 0) |
| goto cleanup; |
| |
| if (git_config_get_string(&old, config, key.ptr) < 0) |
| giterr_clear(); |
| |
| if (!old && only_existing) |
| goto cleanup; |
| if (old && !overwrite) |
| goto cleanup; |
| if ((!old && !value) || (old && value && strcmp(old, value) == 0)) |
| goto cleanup; |
| |
| if (!value) |
| error = git_config_delete_entry(config, key.ptr); |
| else |
| error = git_config_set_string(config, key.ptr, value); |
| |
| cleanup: |
| git_buf_free(&key); |
| return error; |
| } |
| |
| static int submodule_index_status(unsigned int *status, git_submodule *sm) |
| { |
| const git_oid *head_oid = git_submodule_head_id(sm); |
| const git_oid *index_oid = git_submodule_index_id(sm); |
| |
| if (!head_oid) { |
| if (index_oid) |
| *status |= GIT_SUBMODULE_STATUS_INDEX_ADDED; |
| } |
| else if (!index_oid) |
| *status |= GIT_SUBMODULE_STATUS_INDEX_DELETED; |
| else if (!git_oid_equal(head_oid, index_oid)) |
| *status |= GIT_SUBMODULE_STATUS_INDEX_MODIFIED; |
| |
| return 0; |
| } |
| |
| static int submodule_wd_status(unsigned int *status, git_submodule *sm) |
| { |
| int error = 0; |
| const git_oid *wd_oid, *index_oid; |
| git_repository *sm_repo = NULL; |
| |
| /* open repo now if we need it (so wd_id() call won't reopen) */ |
| if ((sm->ignore == GIT_SUBMODULE_IGNORE_NONE || |
| sm->ignore == GIT_SUBMODULE_IGNORE_UNTRACKED) && |
| (sm->flags & GIT_SUBMODULE_STATUS_IN_WD) != 0) |
| { |
| if ((error = git_submodule_open(&sm_repo, sm)) < 0) |
| return error; |
| } |
| |
| index_oid = git_submodule_index_id(sm); |
| wd_oid = git_submodule_wd_id(sm); |
| |
| if (!index_oid) { |
| if (wd_oid) |
| *status |= GIT_SUBMODULE_STATUS_WD_ADDED; |
| } |
| else if (!wd_oid) { |
| if ((sm->flags & GIT_SUBMODULE_STATUS__WD_SCANNED) != 0 && |
| (sm->flags & GIT_SUBMODULE_STATUS_IN_WD) == 0) |
| *status |= GIT_SUBMODULE_STATUS_WD_UNINITIALIZED; |
| else |
| *status |= GIT_SUBMODULE_STATUS_WD_DELETED; |
| } |
| else if (!git_oid_equal(index_oid, wd_oid)) |
| *status |= GIT_SUBMODULE_STATUS_WD_MODIFIED; |
| |
| if (sm_repo != NULL) { |
| git_tree *sm_head; |
| git_diff_options opt = GIT_DIFF_OPTIONS_INIT; |
| git_diff_list *diff; |
| |
| /* the diffs below could be optimized with an early termination |
| * option to the git_diff functions, but for now this is sufficient |
| * (and certainly no worse that what core git does). |
| */ |
| |
| /* perform head-to-index diff on submodule */ |
| |
| if ((error = git_repository_head_tree(&sm_head, sm_repo)) < 0) |
| return error; |
| |
| if (sm->ignore == GIT_SUBMODULE_IGNORE_NONE) |
| opt.flags |= GIT_DIFF_INCLUDE_UNTRACKED; |
| |
| error = git_diff_tree_to_index(&diff, sm_repo, sm_head, NULL, &opt); |
| |
| if (!error) { |
| if (git_diff_num_deltas(diff) > 0) |
| *status |= GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED; |
| |
| git_diff_list_free(diff); |
| diff = NULL; |
| } |
| |
| git_tree_free(sm_head); |
| |
| if (error < 0) |
| return error; |
| |
| /* perform index-to-workdir diff on submodule */ |
| |
| error = git_diff_index_to_workdir(&diff, sm_repo, NULL, &opt); |
| |
| if (!error) { |
| size_t untracked = |
| git_diff_num_deltas_of_type(diff, GIT_DELTA_UNTRACKED); |
| |
| if (untracked > 0) |
| *status |= GIT_SUBMODULE_STATUS_WD_UNTRACKED; |
| |
| if (git_diff_num_deltas(diff) != untracked) |
| *status |= GIT_SUBMODULE_STATUS_WD_WD_MODIFIED; |
| |
| git_diff_list_free(diff); |
| diff = NULL; |
| } |
| |
| git_repository_free(sm_repo); |
| } |
| |
| return error; |
| } |