/*
 * 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 "branch.h"

#include "buf.h"
#include "commit.h"
#include "tag.h"
#include "config.h"
#include "refspec.h"
#include "refs.h"
#include "remote.h"
#include "annotated_commit.h"
#include "worktree.h"

#include "git2/branch.h"

static int retrieve_branch_reference(
	git_reference **branch_reference_out,
	git_repository *repo,
	const char *branch_name,
	bool is_remote)
{
	git_reference *branch = NULL;
	int error = 0;
	char *prefix;
	git_str ref_name = GIT_STR_INIT;

	prefix = is_remote ? GIT_REFS_REMOTES_DIR : GIT_REFS_HEADS_DIR;

	if ((error = git_str_joinpath(&ref_name, prefix, branch_name)) < 0)
		/* OOM */;
	else if ((error = git_reference_lookup(&branch, repo, ref_name.ptr)) < 0)
		git_error_set(
			GIT_ERROR_REFERENCE, "cannot locate %s branch '%s'",
			is_remote ? "remote-tracking" : "local", branch_name);

	*branch_reference_out = branch; /* will be NULL on error */

	git_str_dispose(&ref_name);
	return error;
}

static int not_a_local_branch(const char *reference_name)
{
	git_error_set(
		GIT_ERROR_INVALID,
		"reference '%s' is not a local branch.", reference_name);
	return -1;
}

static bool branch_name_is_valid(const char *branch_name)
{
	/*
	 * Discourage branch name starting with dash,
	 * https://github.com/git/git/commit/6348624010888b
	 * and discourage HEAD as branch name,
	 * https://github.com/git/git/commit/a625b092cc5994
	 */
	return branch_name[0] != '-' && git__strcmp(branch_name, "HEAD");
}

static int create_branch(
	git_reference **ref_out,
	git_repository *repository,
	const char *branch_name,
	const git_commit *commit,
	const char *from,
	int force)
{
	int is_unmovable_head = 0;
	git_reference *branch = NULL;
	git_str canonical_branch_name = GIT_STR_INIT,
			  log_message = GIT_STR_INIT;
	int error = -1;
	int bare = git_repository_is_bare(repository);

	GIT_ASSERT_ARG(branch_name);
	GIT_ASSERT_ARG(commit);
	GIT_ASSERT_ARG(ref_out);
	GIT_ASSERT_ARG(git_commit_owner(commit) == repository);

	if (!branch_name_is_valid(branch_name)) {
		git_error_set(GIT_ERROR_REFERENCE, "'%s' is not a valid branch name", branch_name);
		error = -1;
		goto cleanup;
	}

	if (force && !bare && git_branch_lookup(&branch, repository, branch_name, GIT_BRANCH_LOCAL) == 0) {
		error = git_branch_is_head(branch);
		git_reference_free(branch);
		branch = NULL;

		if (error < 0)
			goto cleanup;

		is_unmovable_head = error;
	}

	if (is_unmovable_head && force) {
		git_error_set(GIT_ERROR_REFERENCE, "cannot force update branch '%s' as it is "
			"the current HEAD of the repository.", branch_name);
		error = -1;
		goto cleanup;
	}

	if (git_str_joinpath(&canonical_branch_name, GIT_REFS_HEADS_DIR, branch_name) < 0)
		goto cleanup;

	if (git_str_printf(&log_message, "branch: Created from %s", from) < 0)
		goto cleanup;

	error = git_reference_create(&branch, repository,
		git_str_cstr(&canonical_branch_name), git_commit_id(commit), force,
		git_str_cstr(&log_message));

	if (!error)
		*ref_out = branch;

cleanup:
	git_str_dispose(&canonical_branch_name);
	git_str_dispose(&log_message);
	return error;
}

int git_branch_create(
	git_reference **ref_out,
	git_repository *repository,
	const char *branch_name,
	const git_commit *commit,
	int force)
{
	char commit_id[GIT_OID_HEXSZ + 1];

	git_oid_tostr(commit_id, GIT_OID_HEXSZ + 1, git_commit_id(commit));
	return create_branch(ref_out, repository, branch_name, commit, commit_id, force);
}

int git_branch_create_from_annotated(
	git_reference **ref_out,
	git_repository *repository,
	const char *branch_name,
	const git_annotated_commit *commit,
	int force)
{
	return create_branch(ref_out,
		repository, branch_name, commit->commit, commit->description, force);
}

static int branch_is_checked_out(git_repository *worktree, void *payload)
{
	git_reference *branch = (git_reference *) payload;
	git_reference *head = NULL;
	int error;

	if (git_repository_is_bare(worktree))
		return 0;

	if ((error = git_reference_lookup(&head, worktree, GIT_HEAD_FILE)) < 0) {
		if (error == GIT_ENOTFOUND)
			error = 0;
		goto out;
	}

	if (git_reference_type(head) != GIT_REFERENCE_SYMBOLIC)
		goto out;

	error = !git__strcmp(head->target.symbolic, branch->name);

out:
	git_reference_free(head);
	return error;
}

int git_branch_is_checked_out(const git_reference *branch)
{
	GIT_ASSERT_ARG(branch);

	if (!git_reference_is_branch(branch))
		return 0;
	return git_repository_foreach_worktree(git_reference_owner(branch),
					       branch_is_checked_out, (void *)branch) == 1;
}

int git_branch_delete(git_reference *branch)
{
	int is_head;
	git_str config_section = GIT_STR_INIT;
	int error = -1;

	GIT_ASSERT_ARG(branch);

	if (!git_reference_is_branch(branch) && !git_reference_is_remote(branch)) {
		git_error_set(GIT_ERROR_INVALID, "reference '%s' is not a valid branch.",
			git_reference_name(branch));
		return GIT_ENOTFOUND;
	}

	if ((is_head = git_branch_is_head(branch)) < 0)
		return is_head;

	if (is_head) {
		git_error_set(GIT_ERROR_REFERENCE, "cannot delete branch '%s' as it is "
			"the current HEAD of the repository.", git_reference_name(branch));
		return -1;
	}

	if (git_reference_is_branch(branch) && git_branch_is_checked_out(branch)) {
		git_error_set(GIT_ERROR_REFERENCE, "Cannot delete branch '%s' as it is "
			"the current HEAD of a linked repository.", git_reference_name(branch));
		return -1;
	}

	if (git_str_join(&config_section, '.', "branch",
			git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR)) < 0)
		goto on_error;

	if (git_config_rename_section(
		git_reference_owner(branch), git_str_cstr(&config_section), NULL) < 0)
		goto on_error;

	error = git_reference_delete(branch);

on_error:
	git_str_dispose(&config_section);
	return error;
}

typedef struct {
	git_reference_iterator *iter;
	unsigned int flags;
} branch_iter;

int git_branch_next(git_reference **out, git_branch_t *out_type, git_branch_iterator *_iter)
{
	branch_iter *iter = (branch_iter *) _iter;
	git_reference *ref;
	int error;

	while ((error = git_reference_next(&ref, iter->iter)) == 0) {
		if ((iter->flags & GIT_BRANCH_LOCAL) &&
		    !git__prefixcmp(ref->name, GIT_REFS_HEADS_DIR)) {
			*out = ref;
			*out_type = GIT_BRANCH_LOCAL;

			return 0;
		} else  if ((iter->flags & GIT_BRANCH_REMOTE) &&
			    !git__prefixcmp(ref->name, GIT_REFS_REMOTES_DIR)) {
			*out = ref;
			*out_type = GIT_BRANCH_REMOTE;

			return 0;
		} else {
			git_reference_free(ref);
		}
	}

	return error;
}

int git_branch_iterator_new(
	git_branch_iterator **out,
	git_repository *repo,
	git_branch_t list_flags)
{
	branch_iter *iter;

	iter = git__calloc(1, sizeof(branch_iter));
	GIT_ERROR_CHECK_ALLOC(iter);

	iter->flags = list_flags;

	if (git_reference_iterator_new(&iter->iter, repo) < 0) {
		git__free(iter);
		return -1;
	}

	*out = (git_branch_iterator *) iter;

	return 0;
}

void git_branch_iterator_free(git_branch_iterator *_iter)
{
	branch_iter *iter = (branch_iter *) _iter;

	if (iter == NULL)
		return;

	git_reference_iterator_free(iter->iter);
	git__free(iter);
}

int git_branch_move(
	git_reference **out,
	git_reference *branch,
	const char *new_branch_name,
	int force)
{
	git_str new_reference_name = GIT_STR_INIT,
	        old_config_section = GIT_STR_INIT,
	        new_config_section = GIT_STR_INIT,
	        log_message = GIT_STR_INIT;
	int error;

	GIT_ASSERT_ARG(branch);
	GIT_ASSERT_ARG(new_branch_name);

	if (!git_reference_is_branch(branch))
		return not_a_local_branch(git_reference_name(branch));

	if ((error = git_str_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name)) < 0)
		goto done;

	if ((error = git_str_printf(&log_message, "branch: renamed %s to %s",
				    git_reference_name(branch), git_str_cstr(&new_reference_name))) < 0)
			goto done;

	/* first update ref then config so failure won't trash config */

	error = git_reference_rename(
		out, branch, git_str_cstr(&new_reference_name), force,
		git_str_cstr(&log_message));
	if (error < 0)
		goto done;

	git_str_join(&old_config_section, '.', "branch",
		git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR));
	git_str_join(&new_config_section, '.', "branch", new_branch_name);

	error = git_config_rename_section(
		git_reference_owner(branch),
		git_str_cstr(&old_config_section),
		git_str_cstr(&new_config_section));

done:
	git_str_dispose(&new_reference_name);
	git_str_dispose(&old_config_section);
	git_str_dispose(&new_config_section);
	git_str_dispose(&log_message);

	return error;
}

int git_branch_lookup(
	git_reference **ref_out,
	git_repository *repo,
	const char *branch_name,
	git_branch_t branch_type)
{
	int error = -1;

	GIT_ASSERT_ARG(ref_out);
	GIT_ASSERT_ARG(repo);
	GIT_ASSERT_ARG(branch_name);

	switch (branch_type) {
	case GIT_BRANCH_LOCAL:
	case GIT_BRANCH_REMOTE:
		error = retrieve_branch_reference(ref_out, repo, branch_name, branch_type == GIT_BRANCH_REMOTE);
		break;
	case GIT_BRANCH_ALL:
		error = retrieve_branch_reference(ref_out, repo, branch_name, false);
		if (error == GIT_ENOTFOUND)
			error = retrieve_branch_reference(ref_out, repo, branch_name, true);
		break;
	default:
		GIT_ASSERT(false);
	}
	return error;
}

int git_branch_name(
	const char **out,
	const git_reference *ref)
{
	const char *branch_name;

	GIT_ASSERT_ARG(out);
	GIT_ASSERT_ARG(ref);

	branch_name = ref->name;

	if (git_reference_is_branch(ref)) {
		branch_name += strlen(GIT_REFS_HEADS_DIR);
	} else if (git_reference_is_remote(ref)) {
		branch_name += strlen(GIT_REFS_REMOTES_DIR);
	} else {
		git_error_set(GIT_ERROR_INVALID,
				"reference '%s' is neither a local nor a remote branch.", ref->name);
		return -1;
	}
	*out = branch_name;
	return 0;
}

static int retrieve_upstream_configuration(
	git_str *out,
	const git_config *config,
	const char *canonical_branch_name,
	const char *format)
{
	git_str buf = GIT_STR_INIT;
	int error;

	if (git_str_printf(&buf, format,
		canonical_branch_name + strlen(GIT_REFS_HEADS_DIR)) < 0)
			return -1;

	error = git_config__get_string_buf(out, config, git_str_cstr(&buf));
	git_str_dispose(&buf);
	return error;
}

int git_branch_upstream_name(
	git_buf *out,
	git_repository *repo,
	const char *refname)
{
	GIT_BUF_WRAP_PRIVATE(out, git_branch__upstream_name, repo, refname);
}

int git_branch__upstream_name(
	git_str *out,
	git_repository *repo,
	const char *refname)
{
	git_str remote_name = GIT_STR_INIT;
	git_str merge_name = GIT_STR_INIT;
	git_str buf = GIT_STR_INIT;
	int error = -1;
	git_remote *remote = NULL;
	const git_refspec *refspec;
	git_config *config;

	GIT_ASSERT_ARG(out);
	GIT_ASSERT_ARG(repo);
	GIT_ASSERT_ARG(refname);

	if (!git_reference__is_branch(refname))
		return not_a_local_branch(refname);

	if ((error = git_repository_config_snapshot(&config, repo)) < 0)
		return error;

	if ((error = retrieve_upstream_configuration(
		&remote_name, config, refname, "branch.%s.remote")) < 0)
			goto cleanup;

	if ((error = retrieve_upstream_configuration(
		&merge_name, config, refname, "branch.%s.merge")) < 0)
			goto cleanup;

	if (git_str_len(&remote_name) == 0 || git_str_len(&merge_name) == 0) {
		git_error_set(GIT_ERROR_REFERENCE,
			"branch '%s' does not have an upstream", refname);
		error = GIT_ENOTFOUND;
		goto cleanup;
	}

	if (strcmp(".", git_str_cstr(&remote_name)) != 0) {
		if ((error = git_remote_lookup(&remote, repo, git_str_cstr(&remote_name))) < 0)
			goto cleanup;

		refspec = git_remote__matching_refspec(remote, git_str_cstr(&merge_name));
		if (!refspec) {
			error = GIT_ENOTFOUND;
			goto cleanup;
		}

		if (git_refspec__transform(&buf, refspec, git_str_cstr(&merge_name)) < 0)
			goto cleanup;
	} else
		if (git_str_set(&buf, git_str_cstr(&merge_name), git_str_len(&merge_name)) < 0)
			goto cleanup;

	git_str_swap(out, &buf);

cleanup:
	git_config_free(config);
	git_remote_free(remote);
	git_str_dispose(&remote_name);
	git_str_dispose(&merge_name);
	git_str_dispose(&buf);
	return error;
}

static int git_branch_upstream_with_format(
	git_str *out,
	git_repository *repo,
	const char *refname,
	const char *format,
	const char *format_name)
{
	git_config *cfg;
	int error;

	if (!git_reference__is_branch(refname))
		return not_a_local_branch(refname);

	if ((error = git_repository_config__weakptr(&cfg, repo)) < 0 ||
	    (error = retrieve_upstream_configuration(out, cfg, refname, format)) < 0)
		return error;

	if (git_str_len(out) == 0) {
		git_error_set(GIT_ERROR_REFERENCE, "branch '%s' does not have an upstream %s", refname, format_name);
		error = GIT_ENOTFOUND;
	}

	return error;
}

int git_branch_upstream_remote(
	git_buf *out,
	git_repository *repo,
	const char *refname)
{
	GIT_BUF_WRAP_PRIVATE(out, git_branch__upstream_remote, repo, refname);
}

int git_branch__upstream_remote(
	git_str *out,
	git_repository *repo,
	const char *refname)
{
	return git_branch_upstream_with_format(out, repo, refname, "branch.%s.remote", "remote");
}

int git_branch_upstream_merge(
	git_buf *out,
	git_repository *repo,
	const char *refname)
{
	GIT_BUF_WRAP_PRIVATE(out, git_branch__upstream_merge, repo, refname);
}

int git_branch__upstream_merge(
	git_str *out,
	git_repository *repo,
	const char *refname)
{
	return git_branch_upstream_with_format(out, repo, refname, "branch.%s.merge", "merge");
}

int git_branch_remote_name(
	git_buf *out,
	git_repository *repo,
	const char *refname)
{
	GIT_BUF_WRAP_PRIVATE(out, git_branch__remote_name, repo, refname);
}

int git_branch__remote_name(
	git_str *out,
	git_repository *repo,
	const char *refname)
{
	git_strarray remote_list = {0};
	size_t i;
	git_remote *remote;
	const git_refspec *fetchspec;
	int error = 0;
	char *remote_name = NULL;

	GIT_ASSERT_ARG(out);
	GIT_ASSERT_ARG(repo);
	GIT_ASSERT_ARG(refname);

	/* Verify that this is a remote branch */
	if (!git_reference__is_remote(refname)) {
		git_error_set(GIT_ERROR_INVALID, "reference '%s' is not a remote branch.",
			refname);
		error = GIT_ERROR;
		goto cleanup;
	}

	/* Get the remotes */
	if ((error = git_remote_list(&remote_list, repo)) < 0)
		goto cleanup;

	/* Find matching remotes */
	for (i = 0; i < remote_list.count; i++) {
		if ((error = git_remote_lookup(&remote, repo, remote_list.strings[i])) < 0)
			continue;

		fetchspec = git_remote__matching_dst_refspec(remote, refname);
		if (fetchspec) {
			/* If we have not already set out yet, then set
			 * it to the matching remote name. Otherwise
			 * multiple remotes match this reference, and it
			 * is ambiguous. */
			if (!remote_name) {
				remote_name = remote_list.strings[i];
			} else {
				git_remote_free(remote);

				git_error_set(GIT_ERROR_REFERENCE,
					"reference '%s' is ambiguous", refname);
				error = GIT_EAMBIGUOUS;
				goto cleanup;
			}
		}

		git_remote_free(remote);
	}

	if (remote_name) {
		git_str_clear(out);
		error = git_str_puts(out, remote_name);
	} else {
		git_error_set(GIT_ERROR_REFERENCE,
			"could not determine remote for '%s'", refname);
		error = GIT_ENOTFOUND;
	}

cleanup:
	if (error < 0)
		git_str_dispose(out);

	git_strarray_dispose(&remote_list);
	return error;
}

int git_branch_upstream(
	git_reference **tracking_out,
	const git_reference *branch)
{
	int error;
	git_str tracking_name = GIT_STR_INIT;

	if ((error = git_branch__upstream_name(&tracking_name,
		git_reference_owner(branch), git_reference_name(branch))) < 0)
			return error;

	error = git_reference_lookup(
		tracking_out,
		git_reference_owner(branch),
		git_str_cstr(&tracking_name));

	git_str_dispose(&tracking_name);
	return error;
}

static int unset_upstream(git_config *config, const char *shortname)
{
	git_str buf = GIT_STR_INIT;

	if (git_str_printf(&buf, "branch.%s.remote", shortname) < 0)
		return -1;

	if (git_config_delete_entry(config, git_str_cstr(&buf)) < 0)
		goto on_error;

	git_str_clear(&buf);
	if (git_str_printf(&buf, "branch.%s.merge", shortname) < 0)
		goto on_error;

	if (git_config_delete_entry(config, git_str_cstr(&buf)) < 0)
		goto on_error;

	git_str_dispose(&buf);
	return 0;

on_error:
	git_str_dispose(&buf);
	return -1;
}

int git_branch_set_upstream(git_reference *branch, const char *branch_name)
{
	git_str key = GIT_STR_INIT, remote_name = GIT_STR_INIT, merge_refspec = GIT_STR_INIT;
	git_reference *upstream;
	git_repository *repo;
	git_remote *remote = NULL;
	git_config *config;
	const char *refname, *shortname;
	int local, error;
	const git_refspec *fetchspec;

	refname = git_reference_name(branch);
	if (!git_reference__is_branch(refname))
		return not_a_local_branch(refname);

	if (git_repository_config__weakptr(&config, git_reference_owner(branch)) < 0)
		return -1;

	shortname = refname + strlen(GIT_REFS_HEADS_DIR);

	/* We're unsetting, delegate and bail-out */
	if (branch_name == NULL)
		return unset_upstream(config, shortname);

	repo = git_reference_owner(branch);

	/* First we need to resolve name to a branch */
	if (git_branch_lookup(&upstream, repo, branch_name, GIT_BRANCH_LOCAL) == 0)
		local = 1;
	else if (git_branch_lookup(&upstream, repo, branch_name, GIT_BRANCH_REMOTE) == 0)
		local = 0;
	else {
		git_error_set(GIT_ERROR_REFERENCE,
			"cannot set upstream for branch '%s'", shortname);
		return GIT_ENOTFOUND;
	}

	/*
	 * If it's a local-tracking branch, its remote is "." (as "the local
	 * repository"), and the branch name is simply the refname.
	 * Otherwise we need to figure out what the remote-tracking branch's
	 * name on the remote is and use that.
	 */
	if (local)
		error = git_str_puts(&remote_name, ".");
	else
		error = git_branch__remote_name(&remote_name, repo, git_reference_name(upstream));

	if (error < 0)
		goto on_error;

	/* Update the upstream branch config with the new name */
	if (git_str_printf(&key, "branch.%s.remote", shortname) < 0)
		goto on_error;

	if (git_config_set_string(config, git_str_cstr(&key), git_str_cstr(&remote_name)) < 0)
		goto on_error;

	if (local) {
		/* A local branch uses the upstream refname directly */
		if (git_str_puts(&merge_refspec, git_reference_name(upstream)) < 0)
			goto on_error;
	} else {
		/* We transform the upstream branch name according to the remote's refspecs */
		if (git_remote_lookup(&remote, repo, git_str_cstr(&remote_name)) < 0)
			goto on_error;

		fetchspec = git_remote__matching_dst_refspec(remote, git_reference_name(upstream));
		if (!fetchspec || git_refspec__rtransform(&merge_refspec, fetchspec, git_reference_name(upstream)) < 0)
			goto on_error;

		git_remote_free(remote);
		remote = NULL;
	}

	/* Update the merge branch config with the refspec */
	git_str_clear(&key);
	if (git_str_printf(&key, "branch.%s.merge", shortname) < 0)
		goto on_error;

	if (git_config_set_string(config, git_str_cstr(&key), git_str_cstr(&merge_refspec)) < 0)
		goto on_error;

	git_reference_free(upstream);
	git_str_dispose(&key);
	git_str_dispose(&remote_name);
	git_str_dispose(&merge_refspec);

	return 0;

on_error:
	git_reference_free(upstream);
	git_str_dispose(&key);
	git_str_dispose(&remote_name);
	git_str_dispose(&merge_refspec);
	git_remote_free(remote);

	return -1;
}

int git_branch_is_head(
		const git_reference *branch)
{
	git_reference *head;
	bool is_same = false;
	int error;

	GIT_ASSERT_ARG(branch);

	if (!git_reference_is_branch(branch))
		return false;

	error = git_repository_head(&head, git_reference_owner(branch));

	if (error == GIT_EUNBORNBRANCH || error == GIT_ENOTFOUND)
		return false;

	if (error < 0)
		return -1;

	is_same = strcmp(
		git_reference_name(branch),
		git_reference_name(head)) == 0;

	git_reference_free(head);

	return is_same;
}

int git_branch_name_is_valid(int *valid, const char *name)
{
	git_str ref_name = GIT_STR_INIT;
	int error = 0;

	GIT_ASSERT(valid);

	*valid = 0;

	if (!name || !branch_name_is_valid(name))
		goto done;

	if ((error = git_str_puts(&ref_name, GIT_REFS_HEADS_DIR)) < 0 ||
	    (error = git_str_puts(&ref_name, name)) < 0)
		goto done;

	error = git_reference_name_is_valid(valid, ref_name.ptr);

done:
	git_str_dispose(&ref_name);
	return error;
}
