/* * 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/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" #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); } 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; seen._cmp = 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 /modules// with a * gitlink in the sub-repo workdir directory to that repository * * Old style: sub-repo goes directly into repo//.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; error = git_iterator_current(&entry, i); while (!error && entry != NULL) { 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); } error = git_iterator_advance(&entry, i); } 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; } error = git_iterator_current(&entry, i); while (!error && entry != NULL) { 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); } error = git_iterator_advance(&entry, i); } 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; }