diff options
Diffstat (limited to 'src/libgit2/submodule.c')
-rw-r--r-- | src/libgit2/submodule.c | 2380 |
1 files changed, 2380 insertions, 0 deletions
diff --git a/src/libgit2/submodule.c b/src/libgit2/submodule.c new file mode 100644 index 000000000..0f4f0726c --- /dev/null +++ b/src/libgit2/submodule.c @@ -0,0 +1,2380 @@ +/* + * 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 "submodule.h" + +#include "buf.h" +#include "branch.h" +#include "vector.h" +#include "posix.h" +#include "config_backend.h" +#include "config.h" +#include "repository.h" +#include "tree.h" +#include "iterator.h" +#include "fs_path.h" +#include "str.h" +#include "index.h" +#include "worktree.h" +#include "clone.h" +#include "path.h" + +#include "git2/config.h" +#include "git2/sys/config.h" +#include "git2/types.h" +#include "git2/index.h" + +#define GIT_MODULES_FILE ".gitmodules" + +static git_configmap _sm_update_map[] = { + {GIT_CONFIGMAP_STRING, "checkout", GIT_SUBMODULE_UPDATE_CHECKOUT}, + {GIT_CONFIGMAP_STRING, "rebase", GIT_SUBMODULE_UPDATE_REBASE}, + {GIT_CONFIGMAP_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE}, + {GIT_CONFIGMAP_STRING, "none", GIT_SUBMODULE_UPDATE_NONE}, + {GIT_CONFIGMAP_FALSE, NULL, GIT_SUBMODULE_UPDATE_NONE}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_SUBMODULE_UPDATE_CHECKOUT}, +}; + +static git_configmap _sm_ignore_map[] = { + {GIT_CONFIGMAP_STRING, "none", GIT_SUBMODULE_IGNORE_NONE}, + {GIT_CONFIGMAP_STRING, "untracked", GIT_SUBMODULE_IGNORE_UNTRACKED}, + {GIT_CONFIGMAP_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, + {GIT_CONFIGMAP_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, + {GIT_CONFIGMAP_FALSE, NULL, GIT_SUBMODULE_IGNORE_NONE}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_SUBMODULE_IGNORE_ALL}, +}; + +static git_configmap _sm_recurse_map[] = { + {GIT_CONFIGMAP_STRING, "on-demand", GIT_SUBMODULE_RECURSE_ONDEMAND}, + {GIT_CONFIGMAP_FALSE, NULL, GIT_SUBMODULE_RECURSE_NO}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_SUBMODULE_RECURSE_YES}, +}; + +enum { + CACHE_OK = 0, + CACHE_REFRESH = 1, + CACHE_FLUSH = 2 +}; +enum { + GITMODULES_EXISTING = 0, + GITMODULES_CREATE = 1 +}; + +static int submodule_alloc(git_submodule **out, git_repository *repo, const char *name); +static git_config_backend *open_gitmodules(git_repository *repo, int gitmod); +static int gitmodules_snapshot(git_config **snap, git_repository *repo); +static int get_url_base(git_str *url, git_repository *repo); +static int lookup_head_remote_key(git_str *remote_key, git_repository *repo); +static int lookup_default_remote(git_remote **remote, git_repository *repo); +static int submodule_load_each(const git_config_entry *entry, void *payload); +static int submodule_read_config(git_submodule *sm, git_config *cfg); +static int submodule_load_from_wd_lite(git_submodule *); +static void submodule_get_index_status(unsigned int *, git_submodule *); +static void submodule_get_wd_status(unsigned int *, git_submodule *, git_repository *, git_submodule_ignore_t); +static void submodule_update_from_index_entry(git_submodule *sm, const git_index_entry *ie); +static void submodule_update_from_head_data(git_submodule *sm, mode_t mode, const git_oid *id); + +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_str *key, const char *suffix) +{ + ssize_t idx = git_str_rfind(key, '.'); + git_str_truncate(key, (size_t)(idx + 1)); + return git_str_puts(key, suffix); +} + +/* + * PUBLIC APIS + */ + +static void submodule_set_lookup_error(int error, const char *name) +{ + if (!error) + return; + + git_error_set(GIT_ERROR_SUBMODULE, (error == GIT_ENOTFOUND) ? + "no submodule named '%s'" : + "submodule '%s' has not been added yet", name); +} + +typedef struct { + const char *path; + char *name; +} fbp_data; + +static int find_by_path(const git_config_entry *entry, void *payload) +{ + fbp_data *data = payload; + + if (!strcmp(entry->value, data->path)) { + const char *fdot, *ldot; + fdot = strchr(entry->name, '.'); + ldot = strrchr(entry->name, '.'); + data->name = git__strndup(fdot + 1, ldot - fdot - 1); + GIT_ERROR_CHECK_ALLOC(data->name); + } + + return 0; +} + +/* + * Checks to see if the submodule shares its name with a file or directory that + * already exists on the index. If so, the submodule cannot be added. + */ +static int is_path_occupied(bool *occupied, git_repository *repo, const char *path) +{ + int error = 0; + git_index *index; + git_str dir = GIT_STR_INIT; + *occupied = false; + + if ((error = git_repository_index__weakptr(&index, repo)) < 0) + goto out; + + if ((error = git_index_find(NULL, index, path)) != GIT_ENOTFOUND) { + if (!error) { + git_error_set(GIT_ERROR_SUBMODULE, + "File '%s' already exists in the index", path); + *occupied = true; + } + goto out; + } + + if ((error = git_str_sets(&dir, path)) < 0) + goto out; + + if ((error = git_fs_path_to_dir(&dir)) < 0) + goto out; + + if ((error = git_index_find_prefix(NULL, index, dir.ptr)) != GIT_ENOTFOUND) { + if (!error) { + git_error_set(GIT_ERROR_SUBMODULE, + "Directory '%s' already exists in the index", path); + *occupied = true; + } + goto out; + } + + error = 0; + +out: + git_str_dispose(&dir); + return error; +} + +/** + * Release the name map returned by 'load_submodule_names'. + */ +static void free_submodule_names(git_strmap *names) +{ + const char *key; + char *value; + + if (names == NULL) + return; + + git_strmap_foreach(names, key, value, { + git__free((char *) key); + git__free(value); + }); + git_strmap_free(names); + + return; +} + +/** + * Map submodule paths to names. + * TODO: for some use-cases, this might need case-folding on a + * case-insensitive filesystem + */ +static int load_submodule_names(git_strmap **out, git_repository *repo, git_config *cfg) +{ + const char *key = "submodule\\..*\\.path"; + git_config_iterator *iter = NULL; + git_config_entry *entry; + git_str buf = GIT_STR_INIT; + git_strmap *names; + int isvalid, error; + + *out = NULL; + + if ((error = git_strmap_new(&names)) < 0) + goto out; + + if ((error = git_config_iterator_glob_new(&iter, cfg, key)) < 0) + goto out; + + while ((error = git_config_next(&entry, iter)) == 0) { + const char *fdot, *ldot; + fdot = strchr(entry->name, '.'); + ldot = strrchr(entry->name, '.'); + + if (git_strmap_exists(names, entry->value)) { + git_error_set(GIT_ERROR_SUBMODULE, + "duplicated submodule path '%s'", entry->value); + error = -1; + goto out; + } + + git_str_clear(&buf); + git_str_put(&buf, fdot + 1, ldot - fdot - 1); + isvalid = git_submodule_name_is_valid(repo, buf.ptr, 0); + if (isvalid < 0) { + error = isvalid; + goto out; + } + if (!isvalid) + continue; + + if ((error = git_strmap_set(names, git__strdup(entry->value), git_str_detach(&buf))) < 0) { + git_error_set(GIT_ERROR_NOMEMORY, "error inserting submodule into hash table"); + error = -1; + goto out; + } + } + if (error == GIT_ITEROVER) + error = 0; + + *out = names; + names = NULL; + +out: + free_submodule_names(names); + git_str_dispose(&buf); + git_config_iterator_free(iter); + return error; +} + +int git_submodule_cache_init(git_strmap **out, git_repository *repo) +{ + int error = 0; + git_strmap *cache = NULL; + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + if ((error = git_strmap_new(&cache)) < 0) + return error; + if ((error = git_submodule__map(repo, cache)) < 0) { + git_submodule_cache_free(cache); + return error; + } + *out = cache; + return error; +} + +int git_submodule_cache_free(git_strmap *cache) +{ + git_submodule *sm = NULL; + if (cache == NULL) + return 0; + git_strmap_foreach_value(cache, sm, { + git_submodule_free(sm); + }); + git_strmap_free(cache); + return 0; +} + +int git_submodule_lookup( + git_submodule **out, /* NULL if user only wants to test existence */ + git_repository *repo, + const char *name) /* trailing slash is allowed */ +{ + return git_submodule__lookup_with_cache(out, repo, name, repo->submodule_cache); +} + +int git_submodule__lookup_with_cache( + git_submodule **out, /* NULL if user only wants to test existence */ + git_repository *repo, + const char *name, /* trailing slash is allowed */ + git_strmap *cache) +{ + int error; + unsigned int location; + git_submodule *sm; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if (repo->is_bare) { + git_error_set(GIT_ERROR_SUBMODULE, "cannot get submodules without a working tree"); + return -1; + } + + if (cache != NULL) { + if ((sm = git_strmap_get(cache, name)) != NULL) { + if (out) { + *out = sm; + GIT_REFCOUNT_INC(*out); + } + return 0; + } + } + + if ((error = submodule_alloc(&sm, repo, name)) < 0) + return error; + + if ((error = git_submodule_reload(sm, false)) < 0) { + git_submodule_free(sm); + return error; + } + + if ((error = git_submodule_location(&location, sm)) < 0) { + git_submodule_free(sm); + return error; + } + + /* If it's not configured or we're looking by path */ + if (location == 0 || location == GIT_SUBMODULE_STATUS_IN_WD) { + git_config_backend *mods; + const char *pattern = "submodule\\..*\\.path"; + git_str path = GIT_STR_INIT; + fbp_data data = { NULL, NULL }; + + git_str_puts(&path, name); + while (path.ptr[path.size-1] == '/') { + path.ptr[--path.size] = '\0'; + } + data.path = path.ptr; + + mods = open_gitmodules(repo, GITMODULES_EXISTING); + + if (mods) + error = git_config_backend_foreach_match(mods, pattern, find_by_path, &data); + + git_config_backend_free(mods); + + if (error < 0) { + git_submodule_free(sm); + git_str_dispose(&path); + return error; + } + + if (data.name) { + git__free(sm->name); + sm->name = data.name; + sm->path = git_str_detach(&path); + + /* Try to load again with the right name */ + if ((error = git_submodule_reload(sm, false)) < 0) { + git_submodule_free(sm); + return error; + } + } + + git_str_dispose(&path); + } + + if ((error = git_submodule_location(&location, sm)) < 0) { + git_submodule_free(sm); + return error; + } + + /* If we still haven't found it, do the WD check */ + if (location == 0 || location == GIT_SUBMODULE_STATUS_IN_WD) { + git_submodule_free(sm); + error = GIT_ENOTFOUND; + + /* If it's not configured, we still check if there's a repo at the path */ + if (git_repository_workdir(repo)) { + git_str path = GIT_STR_INIT; + if (git_str_join3(&path, '/', + git_repository_workdir(repo), + name, DOT_GIT) < 0 || + git_path_validate_str_length(NULL, &path) < 0) + return -1; + + if (git_fs_path_exists(path.ptr)) + error = GIT_EEXISTS; + + git_str_dispose(&path); + } + + submodule_set_lookup_error(error, name); + return error; + } + + if (out) + *out = sm; + else + git_submodule_free(sm); + + return 0; +} + +int git_submodule_name_is_valid(git_repository *repo, const char *name, int flags) +{ + git_str buf = GIT_STR_INIT; + int error, isvalid; + + if (flags == 0) + flags = GIT_FS_PATH_REJECT_FILESYSTEM_DEFAULTS; + + /* Avoid allocating a new string if we can avoid it */ + if (strchr(name, '\\') != NULL) { + if ((error = git_fs_path_normalize_slashes(&buf, name)) < 0) + return error; + } else { + git_str_attach_notowned(&buf, name, strlen(name)); + } + + isvalid = git_path_is_valid(repo, buf.ptr, 0, flags); + git_str_dispose(&buf); + + return isvalid; +} + +static void submodule_free_dup(void *sm) +{ + git_submodule_free(sm); +} + +static int submodule_get_or_create(git_submodule **out, git_repository *repo, git_strmap *map, const char *name) +{ + git_submodule *sm = NULL; + int error; + + if ((sm = git_strmap_get(map, name)) != NULL) + goto done; + + /* if the submodule doesn't exist yet in the map, create it */ + if ((error = submodule_alloc(&sm, repo, name)) < 0) + return error; + + if ((error = git_strmap_set(map, sm->name, sm)) < 0) { + git_submodule_free(sm); + return error; + } + +done: + GIT_REFCOUNT_INC(sm); + *out = sm; + return 0; +} + +static int submodules_from_index(git_strmap *map, git_index *idx, git_config *cfg) +{ + int error; + git_iterator *i = NULL; + const git_index_entry *entry; + git_strmap *names; + + if ((error = load_submodule_names(&names, git_index_owner(idx), cfg))) + goto done; + + if ((error = git_iterator_for_index(&i, git_index_owner(idx), idx, NULL)) < 0) + goto done; + + while (!(error = git_iterator_advance(&entry, i))) { + git_submodule *sm; + + if ((sm = git_strmap_get(map, entry->path)) != NULL) { + if (S_ISGITLINK(entry->mode)) + submodule_update_from_index_entry(sm, entry); + else + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE; + } else if (S_ISGITLINK(entry->mode)) { + const char *name; + + if ((name = git_strmap_get(names, entry->path)) == NULL) + name = entry->path; + + if (!submodule_get_or_create(&sm, git_index_owner(idx), map, name)) { + submodule_update_from_index_entry(sm, entry); + git_submodule_free(sm); + } + } + } + + if (error == GIT_ITEROVER) + error = 0; + +done: + git_iterator_free(i); + free_submodule_names(names); + + return error; +} + +static int submodules_from_head(git_strmap *map, git_tree *head, git_config *cfg) +{ + int error; + git_iterator *i = NULL; + const git_index_entry *entry; + git_strmap *names; + + if ((error = load_submodule_names(&names, git_tree_owner(head), cfg))) + goto done; + + if ((error = git_iterator_for_tree(&i, head, NULL)) < 0) + goto done; + + while (!(error = git_iterator_advance(&entry, i))) { + git_submodule *sm; + + if ((sm = git_strmap_get(map, entry->path)) != NULL) { + if (S_ISGITLINK(entry->mode)) + submodule_update_from_head_data(sm, entry->mode, &entry->id); + else + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE; + } else if (S_ISGITLINK(entry->mode)) { + const char *name; + + if ((name = git_strmap_get(names, entry->path)) == NULL) + name = entry->path; + + if (!submodule_get_or_create(&sm, git_tree_owner(head), map, name)) { + submodule_update_from_head_data( + sm, entry->mode, &entry->id); + git_submodule_free(sm); + } + } + } + + if (error == GIT_ITEROVER) + error = 0; + +done: + git_iterator_free(i); + free_submodule_names(names); + + return error; +} + +/* If have_sm is true, sm is populated, otherwise map an repo are. */ +typedef struct { + git_config *mods; + git_strmap *map; + git_repository *repo; +} lfc_data; + +int git_submodule__map(git_repository *repo, git_strmap *map) +{ + int error = 0; + git_index *idx = NULL; + git_tree *head = NULL; + git_str path = GIT_STR_INIT; + git_submodule *sm; + git_config *mods = NULL; + bool has_workdir; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(map); + + /* get sources that we will need to check */ + if (git_repository_index(&idx, repo) < 0) + git_error_clear(); + if (git_repository_head_tree(&head, repo) < 0) + git_error_clear(); + + has_workdir = git_repository_workdir(repo) != NULL; + + if (has_workdir && + (error = git_repository_workdir_path(&path, repo, GIT_MODULES_FILE)) < 0) + goto cleanup; + + /* add submodule information from .gitmodules */ + if (has_workdir) { + lfc_data data = { 0 }; + data.map = map; + data.repo = repo; + + if ((error = gitmodules_snapshot(&mods, repo)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + goto cleanup; + } + + data.mods = mods; + if ((error = git_config_foreach( + mods, submodule_load_each, &data)) < 0) + goto cleanup; + } + /* add back submodule information from index */ + if (mods && idx) { + if ((error = submodules_from_index(map, idx, mods)) < 0) + goto cleanup; + } + /* add submodule information from HEAD */ + if (mods && head) { + if ((error = submodules_from_head(map, head, mods)) < 0) + goto cleanup; + } + /* shallow scan submodules in work tree as needed */ + if (has_workdir) { + git_strmap_foreach_value(map, sm, { + submodule_load_from_wd_lite(sm); + }); + } + +cleanup: + git_config_free(mods); + /* TODO: if we got an error, mark submodule config as invalid? */ + git_index_free(idx); + git_tree_free(head); + git_str_dispose(&path); + return error; +} + +int git_submodule_foreach( + git_repository *repo, + git_submodule_cb callback, + void *payload) +{ + git_vector snapshot = GIT_VECTOR_INIT; + git_strmap *submodules; + git_submodule *sm; + int error; + size_t i; + + if (repo->is_bare) { + git_error_set(GIT_ERROR_SUBMODULE, "cannot get submodules without a working tree"); + return -1; + } + + if ((error = git_strmap_new(&submodules)) < 0) + return error; + + if ((error = git_submodule__map(repo, submodules)) < 0) + goto done; + + if (!(error = git_vector_init( + &snapshot, git_strmap_size(submodules), submodule_cmp))) { + + git_strmap_foreach_value(submodules, sm, { + if ((error = git_vector_insert(&snapshot, sm)) < 0) + break; + GIT_REFCOUNT_INC(sm); + }); + } + + if (error < 0) + goto done; + + git_vector_uniq(&snapshot, submodule_free_dup); + + git_vector_foreach(&snapshot, i, sm) { + if ((error = callback(sm, sm->name, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + +done: + git_vector_foreach(&snapshot, i, sm) + git_submodule_free(sm); + git_vector_free(&snapshot); + + git_strmap_foreach_value(submodules, sm, { + git_submodule_free(sm); + }); + git_strmap_free(submodules); + + return error; +} + +static int submodule_repo_init( + git_repository **out, + git_repository *parent_repo, + const char *path, + const char *url, + bool use_gitlink) +{ + int error = 0; + git_str workdir = GIT_STR_INIT, repodir = GIT_STR_INIT; + git_repository_init_options initopt = GIT_REPOSITORY_INIT_OPTIONS_INIT; + git_repository *subrepo = NULL; + + error = git_repository_workdir_path(&workdir, parent_repo, path); + if (error < 0) + goto cleanup; + + initopt.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_NO_REINIT; + initopt.origin_url = url; + + /* init submodule repository and add origin remote as needed */ + + /* 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/ + */ + if (use_gitlink) { + error = git_repository__item_path(&repodir, parent_repo, GIT_REPOSITORY_ITEM_MODULES); + if (error < 0) + goto cleanup; + error = git_str_joinpath(&repodir, repodir.ptr, path); + if (error < 0) + goto cleanup; + + initopt.workdir_path = workdir.ptr; + initopt.flags |= + GIT_REPOSITORY_INIT_NO_DOTGIT_DIR | + GIT_REPOSITORY_INIT_RELATIVE_GITLINK; + + error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); + } else + error = git_repository_init_ext(&subrepo, workdir.ptr, &initopt); + +cleanup: + git_str_dispose(&workdir); + git_str_dispose(&repodir); + + *out = subrepo; + + return error; +} + +static int git_submodule__resolve_url( + git_str *out, + git_repository *repo, + const char *url) +{ + int error = 0; + git_str normalized = GIT_STR_INIT; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(url); + + /* We do this in all platforms in case someone on Windows created the .gitmodules */ + if (strchr(url, '\\')) { + if ((error = git_fs_path_normalize_slashes(&normalized, url)) < 0) + return error; + + url = normalized.ptr; + } + + + if (git_fs_path_is_relative(url)) { + if (!(error = get_url_base(out, repo))) + error = git_fs_path_apply_relative(out, url); + } else if (strchr(url, ':') != NULL || url[0] == '/') { + error = git_str_sets(out, url); + } else { + git_error_set(GIT_ERROR_SUBMODULE, "invalid format for submodule URL"); + error = -1; + } + + git_str_dispose(&normalized); + return error; +} + +int git_submodule_resolve_url( + git_buf *out, + git_repository *repo, + const char *url) +{ + GIT_BUF_WRAP_PRIVATE(out, git_submodule__resolve_url, repo, url); +} + +int git_submodule_add_setup( + git_submodule **out, + git_repository *repo, + const char *url, + const char *path, + int use_gitlink) +{ + int error = 0; + git_config_backend *mods = NULL; + git_submodule *sm = NULL; + git_str name = GIT_STR_INIT, real_url = GIT_STR_INIT; + git_repository *subrepo = NULL; + bool path_occupied; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(url); + GIT_ASSERT_ARG(path); + + /* see if there is already an entry for this submodule */ + + if (git_submodule_lookup(NULL, repo, path) < 0) + git_error_clear(); + else { + git_error_set(GIT_ERROR_SUBMODULE, + "attempt to add submodule '%s' that already exists", path); + return GIT_EEXISTS; + } + + /* validate and normalize path */ + + if (git__prefixcmp(path, git_repository_workdir(repo)) == 0) + path += strlen(git_repository_workdir(repo)); + + if (git_fs_path_root(path) >= 0) { + git_error_set(GIT_ERROR_SUBMODULE, "submodule path must be a relative path"); + error = -1; + goto cleanup; + } + + if ((error = is_path_occupied(&path_occupied, repo, path)) < 0) + goto cleanup; + + if (path_occupied) { + error = GIT_EEXISTS; + goto cleanup; + } + + /* update .gitmodules */ + + if (!(mods = open_gitmodules(repo, GITMODULES_CREATE))) { + git_error_set(GIT_ERROR_SUBMODULE, + "adding submodules to a bare repository is not supported"); + return -1; + } + + if ((error = git_str_printf(&name, "submodule.%s.path", path)) < 0 || + (error = git_config_backend_set_string(mods, name.ptr, path)) < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts(&name, "url")) < 0 || + (error = git_config_backend_set_string(mods, name.ptr, url)) < 0) + goto cleanup; + + git_str_clear(&name); + + /* init submodule repository and add origin remote as needed */ + + error = git_repository_workdir_path(&name, repo, path); + if (error < 0) + goto cleanup; + + /* if the repo does not already exist, then init a new repo and add it. + * Otherwise, just add the existing repo. + */ + if (!(git_fs_path_exists(name.ptr) && + git_fs_path_contains(&name, DOT_GIT))) { + + /* resolve the actual URL to use */ + if ((error = git_submodule__resolve_url(&real_url, repo, url)) < 0) + goto cleanup; + + if ((error = submodule_repo_init(&subrepo, repo, path, real_url.ptr, use_gitlink)) < 0) + goto cleanup; + } + + if ((error = git_submodule_lookup(&sm, repo, path)) < 0) + goto cleanup; + + error = git_submodule_init(sm, false); + +cleanup: + if (error && sm) { + git_submodule_free(sm); + sm = NULL; + } + if (out != NULL) + *out = sm; + + git_config_backend_free(mods); + git_repository_free(subrepo); + git_str_dispose(&real_url); + git_str_dispose(&name); + + return error; +} + +int git_submodule_repo_init( + git_repository **out, + const git_submodule *sm, + int use_gitlink) +{ + int error; + git_repository *sub_repo = NULL; + const char *configured_url; + git_config *cfg = NULL; + git_str buf = GIT_STR_INIT; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(sm); + + /* get the configured remote url of the submodule */ + if ((error = git_str_printf(&buf, "submodule.%s.url", sm->name)) < 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; + + *out = sub_repo; + +done: + git_config_free(cfg); + git_str_dispose(&buf); + return error; +} + +static int clone_return_origin(git_remote **out, git_repository *repo, const char *name, const char *url, void *payload) +{ + GIT_UNUSED(url); + GIT_UNUSED(payload); + return git_remote_lookup(out, repo, name); +} + +static int clone_return_repo(git_repository **out, const char *path, int bare, void *payload) +{ + git_submodule *sm = payload; + + GIT_UNUSED(path); + GIT_UNUSED(bare); + return git_submodule_open(out, sm); +} + +int git_submodule_clone(git_repository **out, git_submodule *submodule, const git_submodule_update_options *given_opts) +{ + int error; + git_repository *clone; + git_str rel_path = GIT_STR_INIT; + git_submodule_update_options sub_opts = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + git_clone_options opts = GIT_CLONE_OPTIONS_INIT; + + GIT_ASSERT_ARG(submodule); + + if (given_opts) + memcpy(&sub_opts, given_opts, sizeof(sub_opts)); + + GIT_ERROR_CHECK_VERSION(&sub_opts, GIT_SUBMODULE_UPDATE_OPTIONS_VERSION, "git_submodule_update_options"); + + memcpy(&opts.checkout_opts, &sub_opts.checkout_opts, sizeof(sub_opts.checkout_opts)); + memcpy(&opts.fetch_opts, &sub_opts.fetch_opts, sizeof(sub_opts.fetch_opts)); + opts.repository_cb = clone_return_repo; + opts.repository_cb_payload = submodule; + opts.remote_cb = clone_return_origin; + opts.remote_cb_payload = submodule; + + error = git_repository_workdir_path(&rel_path, git_submodule_owner(submodule), git_submodule_path(submodule)); + if (error < 0) + goto cleanup; + + error = git_clone__submodule(&clone, git_submodule_url(submodule), git_str_cstr(&rel_path), &opts); + if (error < 0) + goto cleanup; + + if (!out) + git_repository_free(clone); + else + *out = clone; + +cleanup: + git_str_dispose(&rel_path); + + return error; +} + +int git_submodule_add_finalize(git_submodule *sm) +{ + int error; + git_index *index; + + GIT_ASSERT_ARG(sm); + + if ((error = git_repository_index__weakptr(&index, sm->repo)) < 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 *sm_repo = NULL; + git_index *index; + git_str path = GIT_STR_INIT; + git_commit *head; + git_index_entry entry; + struct stat st; + + GIT_ASSERT_ARG(sm); + + /* 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, sm->repo)) < 0 || + (error = git_repository_workdir_path(&path, sm->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) { + git_error_set(GIT_ERROR_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, !(git_index_caps(index) & GIT_INDEX_CAPABILITY_NO_FILEMODE)); + + /* calling git_submodule_open will have set sm->wd_oid if possible */ + if ((sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) == 0) { + git_error_set(GIT_ERROR_SUBMODULE, + "cannot add submodule without HEAD to index"); + error = -1; + goto cleanup; + } + git_oid_cpy(&entry.id, &sm->wd_oid); + + if ((error = git_commit_lookup(&head, sm_repo, &sm->wd_oid)) < 0) + goto cleanup; + + entry.ctime.seconds = (int32_t)git_commit_time(head); + entry.ctime.nanoseconds = 0; + entry.mtime.seconds = (int32_t)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_str_dispose(&path); + return error; +} + +static const char *submodule_update_to_str(git_submodule_update_t update) +{ + int i; + for (i = 0; i < (int)ARRAY_SIZE(_sm_update_map); ++i) + if (_sm_update_map[i].map_value == (int)update) + return _sm_update_map[i].str_match; + return NULL; +} + +git_repository *git_submodule_owner(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->repo; +} + +const char *git_submodule_name(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->name; +} + +const char *git_submodule_path(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->path; +} + +const char *git_submodule_url(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->url; +} + +static int write_var(git_repository *repo, const char *name, const char *var, const char *val) +{ + git_str key = GIT_STR_INIT; + git_config_backend *mods; + int error; + + mods = open_gitmodules(repo, GITMODULES_CREATE); + if (!mods) + return -1; + + if ((error = git_str_printf(&key, "submodule.%s.%s", name, var)) < 0) + goto cleanup; + + if (val) + error = git_config_backend_set_string(mods, key.ptr, val); + else + error = git_config_backend_delete(mods, key.ptr); + + git_str_dispose(&key); + +cleanup: + git_config_backend_free(mods); + return error; +} + +static int write_mapped_var(git_repository *repo, const char *name, git_configmap *maps, size_t nmaps, const char *var, int ival) +{ + git_configmap_t type; + const char *val; + + if (git_config_lookup_map_enum(&type, &val, maps, nmaps, ival) < 0) { + git_error_set(GIT_ERROR_SUBMODULE, "invalid value for %s", var); + return -1; + } + + if (type == GIT_CONFIGMAP_TRUE) + val = "true"; + + return write_var(repo, name, var, val); +} + +const char *git_submodule_branch(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->branch; +} + +int git_submodule_set_branch(git_repository *repo, const char *name, const char *branch) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_var(repo, name, "branch", branch); +} + +int git_submodule_set_url(git_repository *repo, const char *name, const char *url) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(url); + + return write_var(repo, name, "url", url); +} + +const git_oid *git_submodule_index_id(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + + 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) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + + 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) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + + /* load unless we think we have a valid oid */ + 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_bare(&subrepo, submodule)) + git_repository_free(subrepo); + else + git_error_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) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, GIT_SUBMODULE_IGNORE_UNSPECIFIED); + + return (submodule->ignore < GIT_SUBMODULE_IGNORE_NONE) ? + GIT_SUBMODULE_IGNORE_NONE : submodule->ignore; +} + +int git_submodule_set_ignore(git_repository *repo, const char *name, git_submodule_ignore_t ignore) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_mapped_var(repo, name, _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), "ignore", ignore); +} + +git_submodule_update_t git_submodule_update_strategy(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, GIT_SUBMODULE_UPDATE_NONE); + + return (submodule->update < GIT_SUBMODULE_UPDATE_CHECKOUT) ? + GIT_SUBMODULE_UPDATE_CHECKOUT : submodule->update; +} + +int git_submodule_set_update(git_repository *repo, const char *name, git_submodule_update_t update) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_mapped_var(repo, name, _sm_update_map, ARRAY_SIZE(_sm_update_map), "update", update); +} + +git_submodule_recurse_t git_submodule_fetch_recurse_submodules( + git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, GIT_SUBMODULE_RECURSE_NO); + return submodule->fetch_recurse; +} + +int git_submodule_set_fetch_recurse_submodules(git_repository *repo, const char *name, git_submodule_recurse_t recurse) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_mapped_var(repo, name, _sm_recurse_map, ARRAY_SIZE(_sm_recurse_map), "fetchRecurseSubmodules", recurse); +} + +static int submodule_repo_create( + git_repository **out, + git_repository *parent_repo, + const char *path) +{ + int error = 0; + git_str workdir = GIT_STR_INIT, repodir = GIT_STR_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_repository_workdir_path(&workdir, parent_repo, path); + if (error < 0) + goto cleanup; + + initopt.workdir_path = workdir.ptr; + + /** + * Repodir: path to the sub-repo. sub-repo goes in: + * <repo-dir>/modules/<name>/ with a gitlink in the + * sub-repo workdir directory to that repository. + */ + error = git_repository__item_path(&repodir, parent_repo, GIT_REPOSITORY_ITEM_MODULES); + if (error < 0) + goto cleanup; + error = git_str_joinpath(&repodir, repodir.ptr, path); + if (error < 0) + goto cleanup; + + error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); + +cleanup: + git_str_dispose(&workdir); + git_str_dispose(&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_submodule *sm; + + GIT_UNUSED(bare); + + sm = payload; + + return submodule_repo_create(out, sm->repo, path); +} + +int git_submodule_update_options_init(git_submodule_update_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_submodule_update_options, GIT_SUBMODULE_UPDATE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_submodule_update_init_options(git_submodule_update_options *opts, unsigned int version) +{ + return git_submodule_update_options_init(opts, version); +} +#endif + +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_str buf = GIT_STR_INIT; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + git_clone_options clone_options = GIT_CLONE_OPTIONS_INIT; + + GIT_ASSERT_ARG(sm); + + if (_update_options) + memcpy(&update_options, _update_options, sizeof(git_submodule_update_options)); + + GIT_ERROR_CHECK_VERSION(&update_options, GIT_SUBMODULE_UPDATE_OPTIONS_VERSION, "git_submodule_update_options"); + + /* Copy over the remote callbacks */ + memcpy(&clone_options.fetch_opts, &update_options.fetch_opts, sizeof(git_fetch_options)); + + /* Get the status of the submodule to determine if it is already initialized */ + if ((error = git_submodule_status(&submodule_status, sm->repo, sm->name, GIT_SUBMODULE_IGNORE_UNSPECIFIED)) < 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_str_printf(&buf, "submodule.%s.url", git_submodule_name(sm))) < 0) + goto done; + + if ((error = git_config_get_string(&submodule_url, config, git_str_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 (!init) { + git_error_set(GIT_ERROR_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_str_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; + + 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))) < 0 || + (error = git_checkout_head(sub_repo, &update_options.checkout_opts)) != 0) + goto done; + } else { + const git_oid *oid; + + /** + * 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) + goto done; + + if ((oid = git_submodule_index_id(sm)) == NULL) { + git_error_set(GIT_ERROR_SUBMODULE, "could not get ID of submodule in index"); + error = -1; + goto done; + } + + /* Look up the target commit in the submodule. */ + if ((error = git_object_lookup(&target_commit, sub_repo, oid, GIT_OBJECT_COMMIT)) < 0) { + /* If it isn't found then fetch and try again. */ + if (error != GIT_ENOTFOUND || !update_options.allow_fetch || + (error = lookup_default_remote(&remote, sub_repo)) < 0 || + (error = git_remote_fetch(remote, NULL, &update_options.fetch_opts, NULL)) < 0 || + (error = git_object_lookup(&target_commit, sub_repo, git_submodule_index_id(sm), GIT_OBJECT_COMMIT)) < 0) + goto done; + } + + if ((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))) < 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_str_dispose(&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; + const char *val; + git_str key = GIT_STR_INIT, effective_submodule_url = GIT_STR_INIT; + git_config *cfg = NULL; + + if (!sm->url) { + git_error_set(GIT_ERROR_SUBMODULE, + "no URL configured for submodule '%s'", sm->name); + return -1; + } + + if ((error = git_repository_config(&cfg, sm->repo)) < 0) + return error; + + /* write "submodule.NAME.url" */ + + if ((error = git_submodule__resolve_url(&effective_submodule_url, sm->repo, sm->url)) < 0 || + (error = git_str_printf(&key, "submodule.%s.url", sm->name)) < 0 || + (error = git_config__update_entry( + cfg, key.ptr, effective_submodule_url.ptr, overwrite != 0, false)) < 0) + goto cleanup; + + /* write "submodule.NAME.update" if not default */ + + val = (sm->update == GIT_SUBMODULE_UPDATE_CHECKOUT) ? + NULL : submodule_update_to_str(sm->update); + + if ((error = git_str_printf(&key, "submodule.%s.update", sm->name)) < 0 || + (error = git_config__update_entry( + cfg, key.ptr, val, overwrite != 0, false)) < 0) + goto cleanup; + + /* success */ + +cleanup: + git_config_free(cfg); + git_str_dispose(&key); + git_str_dispose(&effective_submodule_url); + + return error; +} + +int git_submodule_sync(git_submodule *sm) +{ + git_str key = GIT_STR_INIT, url = GIT_STR_INIT, remote_name = GIT_STR_INIT; + git_repository *smrepo = NULL; + git_config *cfg = NULL; + int error = 0; + + if (!sm->url) { + git_error_set(GIT_ERROR_SUBMODULE, "no URL configured for submodule '%s'", sm->name); + return -1; + } + + /* copy URL over to config only if it already exists */ + if ((error = git_repository_config__weakptr(&cfg, sm->repo)) < 0 || + (error = git_str_printf(&key, "submodule.%s.url", sm->name)) < 0 || + (error = git_submodule__resolve_url(&url, sm->repo, sm->url)) < 0 || + (error = git_config__update_entry(cfg, key.ptr, url.ptr, true, true)) < 0) + goto out; + + if (!(sm->flags & GIT_SUBMODULE_STATUS_IN_WD)) + goto out; + + /* if submodule exists in the working directory, update remote url */ + if ((error = git_submodule_open(&smrepo, sm)) < 0 || + (error = git_repository_config__weakptr(&cfg, smrepo)) < 0) + goto out; + + if (lookup_head_remote_key(&remote_name, smrepo) == 0) { + if ((error = git_str_join3(&key, '.', "remote", remote_name.ptr, "url")) < 0) + goto out; + } else if ((error = git_str_sets(&key, "remote.origin.url")) < 0) { + goto out; + } + + if ((error = git_config__update_entry(cfg, key.ptr, url.ptr, true, false)) < 0) + goto out; + +out: + git_repository_free(smrepo); + git_str_dispose(&remote_name); + git_str_dispose(&key); + git_str_dispose(&url); + return error; +} + +static int git_submodule__open( + git_repository **subrepo, git_submodule *sm, bool bare) +{ + int error; + git_str path = GIT_STR_INIT; + unsigned int flags = GIT_REPOSITORY_OPEN_NO_SEARCH; + const char *wd; + + GIT_ASSERT_ARG(sm); + GIT_ASSERT_ARG(subrepo); + + if (git_repository__ensure_not_bare( + sm->repo, "open submodule repository") < 0) + return GIT_EBAREREPO; + + wd = git_repository_workdir(sm->repo); + + if (git_str_join3(&path, '/', wd, sm->path, DOT_GIT) < 0) + return -1; + + sm->flags = sm->flags & + ~(GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_OID_VALID | + GIT_SUBMODULE_STATUS__WD_SCANNED); + + if (bare) + flags |= GIT_REPOSITORY_OPEN_BARE; + + error = git_repository_open_ext(subrepo, path.ptr, flags, wd); + + /* if we opened the submodule successfully, grab HEAD OID, etc. */ + if (!error) { + sm->flags |= GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_SCANNED; + + if (!git_reference_name_to_id(&sm->wd_oid, *subrepo, GIT_HEAD_FILE)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_OID_VALID; + else + git_error_clear(); + } else if (git_fs_path_exists(path.ptr)) { + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED | + GIT_SUBMODULE_STATUS_IN_WD; + } else { + git_str_rtruncate_at_char(&path, '/'); /* remove "/.git" */ + + if (git_fs_path_isdir(path.ptr)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; + } + + git_str_dispose(&path); + + return error; +} + +int git_submodule_open_bare(git_repository **subrepo, git_submodule *sm) +{ + return git_submodule__open(subrepo, sm, true); +} + +int git_submodule_open(git_repository **subrepo, git_submodule *sm) +{ + return git_submodule__open(subrepo, sm, false); +} + +static void submodule_update_from_index_entry( + git_submodule *sm, const git_index_entry *ie) +{ + bool already_found = (sm->flags & GIT_SUBMODULE_STATUS_IN_INDEX) != 0; + + if (!S_ISGITLINK(ie->mode)) { + if (!already_found) + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE; + } else { + if (already_found) + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES; + else + git_oid_cpy(&sm->index_oid, &ie->id); + + sm->flags |= GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS__INDEX_OID_VALID; + } +} + +static int submodule_update_index(git_submodule *sm) +{ + git_index *index; + const git_index_entry *ie; + + if (git_repository_index__weakptr(&index, sm->repo) < 0) + return -1; + + sm->flags = sm->flags & + ~(GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS__INDEX_OID_VALID); + + if (!(ie = git_index_get_bypath(index, sm->path, 0))) + return 0; + + submodule_update_from_index_entry(sm, ie); + + return 0; +} + +static void submodule_update_from_head_data( + git_submodule *sm, mode_t mode, const git_oid *id) +{ + if (!S_ISGITLINK(mode)) + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE; + else { + git_oid_cpy(&sm->head_oid, id); + + sm->flags |= GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS__HEAD_OID_VALID; + } +} + +static int submodule_update_head(git_submodule *submodule) +{ + git_tree *head = NULL; + git_tree_entry *te = NULL; + + submodule->flags = submodule->flags & + ~(GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS__HEAD_OID_VALID); + + /* if we can't look up file in current head, then done */ + if (git_repository_head_tree(&head, submodule->repo) < 0 || + git_tree_entry_bypath(&te, head, submodule->path) < 0) + git_error_clear(); + else + submodule_update_from_head_data(submodule, te->attr, git_tree_entry_id(te)); + + git_tree_entry_free(te); + git_tree_free(head); + return 0; +} + +int git_submodule_reload(git_submodule *sm, int force) +{ + git_config *mods = NULL; + int error; + + GIT_UNUSED(force); + + GIT_ASSERT_ARG(sm); + + if ((error = git_submodule_name_is_valid(sm->repo, sm->name, 0)) <= 0) + /* This should come with a warning, but we've no API for that */ + goto out; + + if (git_repository_is_bare(sm->repo)) + goto out; + + /* refresh config data */ + if ((error = gitmodules_snapshot(&mods, sm->repo)) < 0 && error != GIT_ENOTFOUND) + goto out; + + if (mods != NULL && (error = submodule_read_config(sm, mods)) < 0) + goto out; + + /* refresh wd data */ + sm->flags &= + ~(GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_OID_VALID | + GIT_SUBMODULE_STATUS__WD_FLAGS); + + if ((error = submodule_load_from_wd_lite(sm)) < 0 || + (error = submodule_update_index(sm)) < 0 || + (error = submodule_update_head(sm)) < 0) + goto out; + +out: + git_config_free(mods); + return error; +} + +static void submodule_copy_oid_maybe( + git_oid *tgt, const git_oid *src, bool valid) +{ + if (tgt) { + if (valid) + memcpy(tgt, src, sizeof(*tgt)); + else + memset(tgt, 0, sizeof(*tgt)); + } +} + +int git_submodule__status( + unsigned int *out_status, + git_oid *out_head_id, + git_oid *out_index_id, + git_oid *out_wd_id, + git_submodule *sm, + git_submodule_ignore_t ign) +{ + unsigned int status; + git_repository *smrepo = NULL; + + if (ign == GIT_SUBMODULE_IGNORE_UNSPECIFIED) + ign = sm->ignore; + + /* only return location info if ignore == all */ + if (ign == GIT_SUBMODULE_IGNORE_ALL) { + *out_status = (sm->flags & GIT_SUBMODULE_STATUS__IN_FLAGS); + return 0; + } + + /* If the user has requested caching submodule state, performing these + * expensive operations (especially `submodule_update_head`, which is + * bottlenecked on `git_repository_head_tree`) eliminates much of the + * advantage. We will, therefore, interpret the request for caching to + * apply here to and skip them. + */ + + if (sm->repo->submodule_cache == NULL) { + /* refresh the index OID */ + if (submodule_update_index(sm) < 0) + return -1; + + /* refresh the HEAD OID */ + if (submodule_update_head(sm) < 0) + return -1; + } + + /* for ignore == dirty, don't scan the working directory */ + if (ign == GIT_SUBMODULE_IGNORE_DIRTY) { + /* git_submodule_open_bare will load WD OID data */ + if (git_submodule_open_bare(&smrepo, sm) < 0) + git_error_clear(); + else + git_repository_free(smrepo); + smrepo = NULL; + } else if (git_submodule_open(&smrepo, sm) < 0) { + git_error_clear(); + smrepo = NULL; + } + + status = GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(sm->flags); + + submodule_get_index_status(&status, sm); + submodule_get_wd_status(&status, sm, smrepo, ign); + + git_repository_free(smrepo); + + *out_status = status; + + submodule_copy_oid_maybe(out_head_id, &sm->head_oid, + (sm->flags & GIT_SUBMODULE_STATUS__HEAD_OID_VALID) != 0); + submodule_copy_oid_maybe(out_index_id, &sm->index_oid, + (sm->flags & GIT_SUBMODULE_STATUS__INDEX_OID_VALID) != 0); + submodule_copy_oid_maybe(out_wd_id, &sm->wd_oid, + (sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) != 0); + + return 0; +} + +int git_submodule_status(unsigned int *status, git_repository *repo, const char *name, git_submodule_ignore_t ignore) +{ + git_submodule *sm; + int error; + + GIT_ASSERT_ARG(status); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = git_submodule_lookup(&sm, repo, name)) < 0) + return error; + + error = git_submodule__status(status, NULL, NULL, NULL, sm, ignore); + git_submodule_free(sm); + + return error; +} + +int git_submodule_location(unsigned int *location, git_submodule *sm) +{ + GIT_ASSERT_ARG(location); + GIT_ASSERT_ARG(sm); + + return git_submodule__status( + location, NULL, NULL, NULL, sm, GIT_SUBMODULE_IGNORE_ALL); +} + +/* + * INTERNAL FUNCTIONS + */ + +static int submodule_alloc( + git_submodule **out, git_repository *repo, const char *name) +{ + size_t namelen; + git_submodule *sm; + + if (!name || !(namelen = strlen(name))) { + git_error_set(GIT_ERROR_SUBMODULE, "invalid submodule name"); + return -1; + } + + sm = git__calloc(1, sizeof(git_submodule)); + GIT_ERROR_CHECK_ALLOC(sm); + + sm->name = sm->path = git__strdup(name); + if (!sm->name) { + git__free(sm); + return -1; + } + + GIT_REFCOUNT_INC(sm); + sm->ignore = sm->ignore_default = GIT_SUBMODULE_IGNORE_NONE; + sm->update = sm->update_default = GIT_SUBMODULE_UPDATE_CHECKOUT; + sm->fetch_recurse = sm->fetch_recurse_default = GIT_SUBMODULE_RECURSE_NO; + sm->repo = repo; + sm->branch = NULL; + + *out = sm; + return 0; +} + +static void submodule_release(git_submodule *sm) +{ + if (!sm) + return; + + if (sm->repo) { + sm->repo = NULL; + } + + if (sm->path != sm->name) + git__free(sm->path); + git__free(sm->name); + git__free(sm->url); + git__free(sm->branch); + git__memzero(sm, sizeof(*sm)); + git__free(sm); +} + +int git_submodule_dup(git_submodule **out, git_submodule *source) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(source); + + GIT_REFCOUNT_INC(source); + + *out = source; + return 0; +} + +void git_submodule_free(git_submodule *sm) +{ + if (!sm) + return; + GIT_REFCOUNT_DEC(sm, submodule_release); +} + +static int submodule_config_error(const char *property, const char *value) +{ + git_error_set(GIT_ERROR_INVALID, + "invalid value for submodule '%s' property: '%s'", property, value); + return -1; +} + +int git_submodule_parse_ignore(git_submodule_ignore_t *out, const char *value) +{ + int val; + + if (git_config_lookup_map_value( + &val, _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value) < 0) { + *out = GIT_SUBMODULE_IGNORE_NONE; + return submodule_config_error("ignore", value); + } + + *out = (git_submodule_ignore_t)val; + return 0; +} + +int git_submodule_parse_update(git_submodule_update_t *out, const char *value) +{ + int val; + + if (git_config_lookup_map_value( + &val, _sm_update_map, ARRAY_SIZE(_sm_update_map), value) < 0) { + *out = GIT_SUBMODULE_UPDATE_CHECKOUT; + return submodule_config_error("update", value); + } + + *out = (git_submodule_update_t)val; + return 0; +} + +static int submodule_parse_recurse(git_submodule_recurse_t *out, const char *value) +{ + int val; + + if (git_config_lookup_map_value( + &val, _sm_recurse_map, ARRAY_SIZE(_sm_recurse_map), value) < 0) { + *out = GIT_SUBMODULE_RECURSE_YES; + return submodule_config_error("recurse", value); + } + + *out = (git_submodule_recurse_t)val; + return 0; +} + +static int get_value(const char **out, git_config *cfg, git_str *buf, const char *name, const char *field) +{ + int error; + + git_str_clear(buf); + + if ((error = git_str_printf(buf, "submodule.%s.%s", name, field)) < 0 || + (error = git_config_get_string(out, cfg, buf->ptr)) < 0) + return error; + + return error; +} + +static bool looks_like_command_line_option(const char *s) +{ + if (s && s[0] == '-') + return true; + + return false; +} + +static int submodule_read_config(git_submodule *sm, git_config *cfg) +{ + git_str key = GIT_STR_INIT; + const char *value; + int error, in_config = 0; + + /* + * 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 ((error = get_value(&value, cfg, &key, sm->name, "path")) == 0) { + in_config = 1; + /* We would warn here if we had that API */ + if (!looks_like_command_line_option(value)) { + /* + * TODO: if case insensitive filesystem, then the following strcmp + * should be strcasecmp + */ + if (strcmp(sm->name, value) != 0) { + if (sm->path != sm->name) + git__free(sm->path); + sm->path = git__strdup(value); + GIT_ERROR_CHECK_ALLOC(sm->path); + } + + } + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "url")) == 0) { + /* We would warn here if we had that API */ + if (!looks_like_command_line_option(value)) { + in_config = 1; + sm->url = git__strdup(value); + GIT_ERROR_CHECK_ALLOC(sm->url); + } + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "branch")) == 0) { + in_config = 1; + sm->branch = git__strdup(value); + GIT_ERROR_CHECK_ALLOC(sm->branch); + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "update")) == 0) { + in_config = 1; + if ((error = git_submodule_parse_update(&sm->update, value)) < 0) + goto cleanup; + sm->update_default = sm->update; + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "fetchRecurseSubmodules")) == 0) { + in_config = 1; + if ((error = submodule_parse_recurse(&sm->fetch_recurse, value)) < 0) + goto cleanup; + sm->fetch_recurse_default = sm->fetch_recurse; + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "ignore")) == 0) { + in_config = 1; + if ((error = git_submodule_parse_ignore(&sm->ignore, value)) < 0) + goto cleanup; + sm->ignore_default = sm->ignore; + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if (in_config) + sm->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; + + error = 0; + +cleanup: + git_str_dispose(&key); + return error; +} + +static int submodule_load_each(const git_config_entry *entry, void *payload) +{ + lfc_data *data = payload; + const char *namestart, *property; + git_strmap *map = data->map; + git_str name = GIT_STR_INIT; + git_submodule *sm; + int error, isvalid; + + if (git__prefixcmp(entry->name, "submodule.") != 0) + return 0; + + namestart = entry->name + strlen("submodule."); + property = strrchr(namestart, '.'); + + if (!property || (property == namestart)) + return 0; + + property++; + + if ((error = git_str_set(&name, namestart, property - namestart -1)) < 0) + return error; + + isvalid = git_submodule_name_is_valid(data->repo, name.ptr, 0); + if (isvalid <= 0) { + error = isvalid; + goto done; + } + + /* + * Now that we have the submodule's name, we can use that to + * figure out whether it's in the map. If it's not, we create + * a new submodule, load the config and insert it. If it's + * already inserted, we've already loaded it, so we skip. + */ + if (git_strmap_exists(map, name.ptr)) { + error = 0; + goto done; + } + + if ((error = submodule_alloc(&sm, data->repo, name.ptr)) < 0) + goto done; + + if ((error = submodule_read_config(sm, data->mods)) < 0) { + git_submodule_free(sm); + goto done; + } + + if ((error = git_strmap_set(map, sm->name, sm)) < 0) + goto done; + + error = 0; + +done: + git_str_dispose(&name); + return error; +} + +static int submodule_load_from_wd_lite(git_submodule *sm) +{ + git_str path = GIT_STR_INIT; + + if (git_repository_workdir_path(&path, sm->repo, sm->path) < 0) + return -1; + + if (git_fs_path_isdir(path.ptr)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; + + if (git_fs_path_contains(&path, DOT_GIT)) + sm->flags |= GIT_SUBMODULE_STATUS_IN_WD; + + git_str_dispose(&path); + return 0; +} + +/** + * Requests a snapshot of $WORK_TREE/.gitmodules. + * + * Returns GIT_ENOTFOUND in case no .gitmodules file exist + */ +static int gitmodules_snapshot(git_config **snap, git_repository *repo) +{ + git_config *mods = NULL; + git_str path = GIT_STR_INIT; + int error; + + if (git_repository_workdir(repo) == NULL) + return GIT_ENOTFOUND; + + if ((error = git_repository_workdir_path(&path, repo, GIT_MODULES_FILE)) < 0) + return error; + + if ((error = git_config_open_ondisk(&mods, path.ptr)) < 0) + goto cleanup; + git_str_dispose(&path); + + if ((error = git_config_snapshot(snap, mods)) < 0) + goto cleanup; + + error = 0; + +cleanup: + if (mods) + git_config_free(mods); + git_str_dispose(&path); + + return error; +} + +static git_config_backend *open_gitmodules( + git_repository *repo, + int okay_to_create) +{ + git_str path = GIT_STR_INIT; + git_config_backend *mods = NULL; + + if (git_repository_workdir(repo) != NULL) { + if (git_repository_workdir_path(&path, repo, GIT_MODULES_FILE) != 0) + return NULL; + + if (okay_to_create || git_fs_path_isfile(path.ptr)) { + /* git_config_backend_from_file should only fail if OOM */ + if (git_config_backend_from_file(&mods, path.ptr) < 0) + mods = NULL; + /* open should only fail here if the file is malformed */ + else if (git_config_backend_open(mods, GIT_CONFIG_LEVEL_LOCAL, repo) < 0) { + git_config_backend_free(mods); + mods = NULL; + } + } + } + + git_str_dispose(&path); + + return mods; +} + +/* Lookup name of remote of the local tracking branch HEAD points to */ +static int lookup_head_remote_key(git_str *remote_name, git_repository *repo) +{ + int error; + git_reference *head = NULL; + git_str upstream_name = GIT_STR_INIT; + + /* lookup and dereference HEAD */ + if ((error = git_repository_head(&head, repo)) < 0) + return error; + + /** + * 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)) { + git_error_set(GIT_ERROR_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_str_dispose(&upstream_name); + git_reference_free(head); + + return error; +} + +/* Lookup the remote of the local tracking branch HEAD points to */ +static int lookup_head_remote(git_remote **remote, git_repository *repo) +{ + int error; + git_str remote_name = GIT_STR_INIT; + + /* lookup remote of remote tracking branch name */ + if (!(error = lookup_head_remote_key(&remote_name, repo))) + error = git_remote_lookup(remote, repo, remote_name.ptr); + + git_str_dispose(&remote_name); + + return error; +} + +/* Lookup remote, either from HEAD or fall back on origin */ +static int lookup_default_remote(git_remote **remote, git_repository *repo) +{ + int error = lookup_head_remote(remote, repo); + + /* if that failed, use 'origin' instead */ + if (error == GIT_ENOTFOUND || error == GIT_EUNBORNBRANCH) + error = git_remote_lookup(remote, repo, "origin"); + + if (error == GIT_ENOTFOUND) + git_error_set( + GIT_ERROR_SUBMODULE, + "cannot get default remote for submodule - no local tracking " + "branch for HEAD and origin does not exist"); + + return error; +} + +static int get_url_base(git_str *url, git_repository *repo) +{ + int error; + git_worktree *wt = NULL; + git_remote *remote = NULL; + + if ((error = lookup_default_remote(&remote, repo)) == 0) { + error = git_str_sets(url, git_remote_url(remote)); + goto out; + } else if (error != GIT_ENOTFOUND) + goto out; + else + git_error_clear(); + + /* if repository does not have a default remote, use workdir instead */ + if (git_repository_is_worktree(repo)) { + if ((error = git_worktree_open_from_repository(&wt, repo)) < 0) + goto out; + error = git_str_sets(url, wt->parent_path); + } else { + error = git_str_sets(url, git_repository_workdir(repo)); + } + +out: + git_remote_free(remote); + git_worktree_free(wt); + + return error; +} + +static void submodule_get_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); + + *status = *status & ~GIT_SUBMODULE_STATUS__INDEX_FLAGS; + + 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; +} + + +static void submodule_get_wd_status( + unsigned int *status, + git_submodule *sm, + git_repository *sm_repo, + git_submodule_ignore_t ign) +{ + const git_oid *index_oid = git_submodule_index_id(sm); + const git_oid *wd_oid = + (sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) ? &sm->wd_oid : NULL; + git_tree *sm_head = NULL; + git_index *index = NULL; + git_diff_options opt = GIT_DIFF_OPTIONS_INIT; + git_diff *diff; + + *status = *status & ~GIT_SUBMODULE_STATUS__WD_FLAGS; + + 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 we have no repo, then we're done */ + if (!sm_repo) + return; + + /* 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). + */ + + if (ign == GIT_SUBMODULE_IGNORE_NONE) + opt.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + + (void)git_repository_index__weakptr(&index, sm_repo); + + /* if we don't have an unborn head, check diff with index */ + if (git_repository_head_tree(&sm_head, sm_repo) < 0) + git_error_clear(); + else { + /* perform head to index diff on submodule */ + if (git_diff_tree_to_index(&diff, sm_repo, sm_head, index, &opt) < 0) + git_error_clear(); + else { + if (git_diff_num_deltas(diff) > 0) + *status |= GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED; + git_diff_free(diff); + diff = NULL; + } + + git_tree_free(sm_head); + } + + /* perform index-to-workdir diff on submodule */ + if (git_diff_index_to_workdir(&diff, sm_repo, index, &opt) < 0) + git_error_clear(); + else { + 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_free(diff); + diff = NULL; + } +} |