/*
 * Copyright (C) 2012 the libgit2 contributors
 *
 * 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, bool force);
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, false)) < 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, false)) < 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(&seen, sm) != GIT_ENOTFOUND)
				continue;
			if ((error = git_vector_insert(&seen, sm)) < 0)
				break;
		}

		if (callback(sm, sm->name, payload)) {
			giterr_clear();
			error = GIT_EUSER;
			break;
		}
	});

	git_vector_free(&seen);

	return error;
}

void git_submodule_config_free(git_repository *repo)
{
	git_strmap *smcfg;
	git_submodule *sm;

	assert(repo);

	smcfg = repo->submodules;
	repo->submodules = NULL;

	if (smcfg == NULL)
		return;

	git_strmap_foreach_value(smcfg, sm, {
		submodule_release(sm,1);
	});
	git_strmap_free(smcfg);
}

int git_submodule_add_setup(
	git_submodule **submodule,
	git_repository *repo,
	const char *url,
	const char *path,
	int use_gitlink)
{
	int error = 0;
	git_config_backend *mods = NULL;
	git_submodule *sm;
	git_buf name = GIT_BUF_INIT, real_url = GIT_BUF_INIT;
	git_repository_init_options initopt = GIT_REPOSITORY_INIT_OPTIONS_INIT;
	git_repository *subrepo = NULL;

	assert(repo && url && path);

	/* see if there is already an entry for this submodule */

	if (git_submodule_lookup(&sm, repo, path) < 0)
		giterr_clear();
	else {
		giterr_set(GITERR_SUBMODULE,
			"Attempt to add a submodule that already exists");
		return GIT_EEXISTS;
	}

	/* resolve parameters */

	if (url[0] == '.' && (url[1] == '/' || (url[1] == '.' && url[2] == '/'))) {
		if (!(error = lookup_head_remote(&real_url, repo)))
			error = git_path_apply_relative(&real_url, url);
	} else if (strchr(url, ':') != NULL || url[0] == '/') {
		error = git_buf_sets(&real_url, url);
	} else {
		giterr_set(GITERR_SUBMODULE, "Invalid format for submodule URL");
		error = -1;
	}
	if (error)
		goto cleanup;

	/* validate and normalize path */

	if (git__prefixcmp(path, git_repository_workdir(repo)) == 0)
		path += strlen(git_repository_workdir(repo));

	if (git_path_root(path) >= 0) {
		giterr_set(GITERR_SUBMODULE, "Submodule path must be a relative path");
		error = -1;
		goto cleanup;
	}

	/* update .gitmodules */

	if ((mods = open_gitmodules(repo, true, NULL)) == NULL) {
		giterr_set(GITERR_SUBMODULE,
			"Adding submodules to a bare repository is not supported (for now)");
		return -1;
	}

	if ((error = git_buf_printf(&name, "submodule.%s.path", path)) < 0 ||
		(error = git_config_file_set_string(mods, name.ptr, path)) < 0)
		goto cleanup;

	if ((error = submodule_config_key_trunc_puts(&name, "url")) < 0 ||
		(error = git_config_file_set_string(mods, name.ptr, real_url.ptr)) < 0)
		goto cleanup;

	git_buf_clear(&name);

	/* init submodule repository and add origin remote as needed */

	error = git_buf_joinpath(&name, git_repository_workdir(repo), path);
	if (error < 0)
		goto cleanup;

	/* New style: sub-repo goes in <repo-dir>/modules/<name>/ with a
	 * gitlink in the sub-repo workdir directory to that repository
	 *
	 * Old style: sub-repo goes directly into repo/<name>/.git/
	 */

	initopt.flags = GIT_REPOSITORY_INIT_MKPATH |
		GIT_REPOSITORY_INIT_NO_REINIT;
	initopt.origin_url = real_url.ptr;

	if (git_path_exists(name.ptr) &&
		git_path_contains(&name, DOT_GIT))
	{
		/* repo appears to already exist - reinit? */
	}
	else if (use_gitlink) {
		git_buf repodir = GIT_BUF_INIT;

		error = git_buf_join_n(
			&repodir, '/', 3, git_repository_path(repo), "modules", path);
		if (error < 0)
			goto cleanup;

		initopt.workdir_path = name.ptr;
		initopt.flags |= GIT_REPOSITORY_INIT_NO_DOTGIT_DIR;

		error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt);

		git_buf_free(&repodir);
	}
	else {
		error = git_repository_init_ext(&subrepo, name.ptr, &initopt);
	}
	if (error < 0)
		goto cleanup;

	/* add submodule to hash and "reload" it */

	if (!(error = submodule_get(&sm, repo, path, NULL)) &&
		!(error = git_submodule_reload(sm)))
		error = git_submodule_init(sm, false);

cleanup:
	if (submodule != NULL)
		*submodule = !error ? sm : NULL;

	if (mods != NULL)
		git_config_file_free(mods);
	git_repository_free(subrepo);
	git_buf_free(&real_url);
	git_buf_free(&name);

	return error;
}

int git_submodule_add_finalize(git_submodule *sm)
{
	int error;
	git_index *index;

	assert(sm);

	if ((error = git_repository_index__weakptr(&index, sm->owner)) < 0 ||
		(error = git_index_add_from_workdir(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 && !(submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID)) {
		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);
	return load_submodule_config(repo, true);
}

int git_submodule_reload(git_submodule *submodule)
{
	git_repository *repo;
	git_index *index;
	int pos, error;
	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);

	pos = git_index_find(index, submodule->path);
	if (pos >= 0) {
		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;
}

/*
 * 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_iterator *i;
	const git_index_entry *entry;

	if ((error = git_iterator_for_repo_index(&i, repo)) < 0)
		return error;

	error = git_iterator_current(i, &entry);

	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(i, &entry);
	}

	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) {
		git_tree_free(head);
		return error;
	}

	error = git_iterator_current(i, &entry);

	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(i, &entry);
	}

	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, bool force)
{
	int error;
	git_oid gitmodules_oid;
	git_buf path = GIT_BUF_INIT;
	git_config_backend *mods = NULL;

	if (repo->submodules && !force)
		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_tracking(&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) > 0)
				*status |= GIT_SUBMODULE_STATUS_WD_WD_MODIFIED;

			git_diff_list_free(diff);
			diff = NULL;
		}

		git_repository_free(sm_repo);
	}

	return error;
}
