#include "managers/git_manager.h"

#include <izo/Paths.hpp>
#include <izo/Time.hpp>

#include <algorithm>
#include <array>
#include <chrono>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>

#ifdef _WIN32
#include <windows.h>
#endif

namespace
{
    std::string trim(std::string value)
    {
        const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char character)
                                            { return std::isspace(character) != 0; });
        const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char character)
                                           { return std::isspace(character) != 0; }).base();
        if (first >= last)
            return {};
        return std::string(first, last);
    }

    bool startsWith(std::string_view text, std::string_view prefix)
    {
        return text.size() >= prefix.size() && text.substr(0, prefix.size()) == prefix;
    }

    bool parseCheckoutReflog(std::string_view subject, std::string &from, std::string &to)
    {
        constexpr std::string_view prefix = "checkout: moving from ";
        if (!startsWith(subject, prefix))
            return false;
        const size_t separator = subject.find(" to ", prefix.size());
        if (separator == std::string_view::npos)
            return false;
        from = trim(std::string(subject.substr(prefix.size(), separator - prefix.size())));
        to = trim(std::string(subject.substr(separator + 4)));
        return !from.empty() && !to.empty();
    }

    std::string commitSummaryFromReflog(std::string_view subject)
    {
        const size_t colon = subject.find(':');
        return colon == std::string_view::npos ? std::string{} : trim(std::string(subject.substr(colon + 1)));
    }

    ToolbarHistoryAction unavailableAction(const char *tooltip)
    {
        ToolbarHistoryAction action;
        action.tooltip = tooltip;
        return action;
    }

    ToolbarHistoryAction makeCheckoutAction(const std::string &from, const std::string &to, const char *verb)
    {
        ToolbarHistoryAction action;
        action.kind = ToolbarHistoryActionKind::checkout;
        action.available = true;
        action.source = from;
        action.target = to;
        action.tooltip = std::string(verb) + " Checkout from \"" + from + "\" to \"" + to + "\"";
        return action;
    }

    ToolbarHistoryAction makeCommitAction(std::string summary, const char *verb)
    {
        ToolbarHistoryAction action;
        action.kind = ToolbarHistoryActionKind::commit;
        action.available = true;
        action.summary = std::move(summary);
        action.tooltip = action.summary.empty() ? std::string(verb) + " Last Commit"
                                                : std::string(verb) + " Commit \"" + action.summary + "\"";
        return action;
    }

    void populateRepositoryIdentity(RepositoryView &repository)
    {
        const char *workdir = git_repository_workdir(repository.repo);
        repository.path = workdir ? workdir : git_repository_path(repository.repo);
        std::filesystem::path path(repository.path);
        repository.name = path.filename().string();
        if (repository.name.empty())
            repository.name = path.parent_path().filename().string();

        repository.branch = "detached";
        git_reference *head = nullptr;
        if (git_repository_head(&head, repository.repo) == 0)
        {
            if (const char *name = git_reference_shorthand(head))
                repository.branch = name;
            git_reference_free(head);
        }
    }

    std::vector<std::string> repositoryRemotes(git_repository *repository)
    {
        std::vector<std::string> remotes;
        git_strarray names{};
        if (git_remote_list(&names, repository) == 0)
        {
            for (size_t i = 0; i < names.count; ++i)
                remotes.emplace_back(names.strings[i]);
            git_strarray_dispose(&names);
        }
        return remotes;
    }

    std::string preferredRemoteName(git_repository *repository)
    {
        const std::vector<std::string> remotes = repositoryRemotes(repository);
        if (remotes.empty())
            return {};
        const auto origin = std::find(remotes.begin(), remotes.end(), "origin");
        return origin != remotes.end() ? *origin : remotes.front();
    }

#ifdef _WIN32
    std::string encodeBase64(std::string_view value)
    {
        static constexpr std::string_view alphabet =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        std::string encoded;
        encoded.reserve(((value.size() + 2) / 3) * 4);
        for (size_t index = 0; index < value.size(); index += 3)
        {
            const uint32_t a = static_cast<unsigned char>(value[index]);
            const uint32_t b = index + 1 < value.size() ? static_cast<unsigned char>(value[index + 1]) : 0;
            const uint32_t c = index + 2 < value.size() ? static_cast<unsigned char>(value[index + 2]) : 0;
            const uint32_t chunk = (a << 16) | (b << 8) | c;
            encoded.push_back(alphabet[(chunk >> 18) & 0x3F]);
            encoded.push_back(alphabet[(chunk >> 12) & 0x3F]);
            encoded.push_back(index + 1 < value.size() ? alphabet[(chunk >> 6) & 0x3F] : '=');
            encoded.push_back(index + 2 < value.size() ? alphabet[chunk & 0x3F] : '=');
        }
        return encoded;
    }
#endif

    std::vector<std::string> withAuthOverrideArguments(std::vector<std::string> arguments,
                                                       const std::optional<GitAuthOverride> &auth)
    {
        arguments.insert(arguments.begin(), {
            "-c", "credential.interactive=never",
        });
        if (!auth || auth->username.empty())
            return arguments;
        const std::string header = "http.extraHeader=Authorization: Basic " +
                                   encodeBase64(auth->username + ":" + auth->password);
        arguments.insert(arguments.begin() + 2, {
            "-c", header,
        });
        return arguments;
    }

    void addBadge(RepositoryView &repository, const git_oid *oid, RefBadge badge)
    {
        if (!oid)
            return;
        const auto commit = std::find_if(repository.commits.begin(), repository.commits.end(),
                                         [oid](const CommitInfo &item)
                                         { return git_oid_equal(&item.oid, oid) != 0; });
        if (commit != repository.commits.end())
            commit->refs.push_back(std::move(badge));
    }

    bool remoteIsCoveredByLocalUpstream(const RepositoryView &repository, const git_oid *oid,
                                        const std::string &remote_name)
    {
        if (!oid)
            return false;
        const size_t slash = remote_name.find('/');
        const std::string local_name = slash == std::string::npos
                                           ? remote_name
                                           : remote_name.substr(slash + 1);
        const auto commit = std::find_if(repository.commits.begin(), repository.commits.end(),
                                         [oid](const CommitInfo &item)
                                         { return git_oid_equal(&item.oid, oid) != 0; });
        if (commit == repository.commits.end())
            return false;
        return std::any_of(commit->refs.begin(), commit->refs.end(), [&local_name](const RefBadge &badge)
                           { return badge.kind == RefKind::local && badge.upstream && badge.name == local_name; });
    }

    int submoduleCallback(git_submodule *submodule, const char *, void *payload)
    {
        auto *repository = static_cast<RepositoryView *>(payload);
        const char *name = git_submodule_name(submodule);
        if (!name)
            return 0;
        repository->submodules.emplace_back(name);
        unsigned int status = 0;
        if (git_submodule_status(&status, repository->repo, name,
                                 GIT_SUBMODULE_IGNORE_UNSPECIFIED) == 0)
            repository->submodule_statuses[name] = status;
        return 0;
    }

    int diffFileCallback(const git_diff_delta *delta, float, void *payload)
    {
        auto *files = static_cast<std::vector<ChangedFile> *>(payload);
        ChangedFile file;
        const char *path = delta->new_file.path ? delta->new_file.path : delta->old_file.path;
        file.path = path ? path : "(unknown)";
        switch (delta->status)
        {
        case GIT_DELTA_ADDED:
            file.kind = FileChangeKind::added;
            break;
        case GIT_DELTA_MODIFIED:
            file.kind = FileChangeKind::modified;
            break;
        case GIT_DELTA_DELETED:
            file.kind = FileChangeKind::deleted;
            break;
        case GIT_DELTA_RENAMED:
            file.kind = FileChangeKind::renamed;
            break;
        default:
            file.kind = FileChangeKind::other;
            break;
        }
        files->push_back(std::move(file));
        return 0;
    }

    int treeFileCallback(const char *root, const git_tree_entry *entry, void *payload)
    {
        const git_object_t type = git_tree_entry_type(entry);
        if (type != GIT_OBJECT_BLOB && type != GIT_OBJECT_COMMIT)
            return 0;
        auto *files = static_cast<std::vector<ChangedFile> *>(payload);
        files->push_back({std::string(root ? root : "") + git_tree_entry_name(entry),
                          FileChangeKind::other});
        return 0;
    }

#ifdef _WIN32
    std::wstring utf8ToWide(const std::string &value)
    {
        if (value.empty())
            return {};
        const int length = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
        std::wstring result(static_cast<size_t>(std::max(0, length)), L'\0');
        if (length > 1)
            MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, result.data(), length);
        if (!result.empty())
            result.pop_back();
        return result;
    }

    std::wstring quoteWindowsArgument(const std::wstring &value)
    {
        if (value.find_first_of(L" \t\n\v\"") == std::wstring::npos)
            return value;
        std::wstring result = L"\"";
        size_t slashes = 0;
        for (const wchar_t character : value)
        {
            if (character == L'\\')
            {
                ++slashes;
            }
            else if (character == L'\"')
            {
                result.append(slashes * 2 + 1, L'\\');
                result.push_back(L'\"');
                slashes = 0;
            }
            else
            {
                result.append(slashes, L'\\');
                slashes = 0;
                result.push_back(character);
            }
        }
        result.append(slashes * 2, L'\\');
        result.push_back(L'\"');
        return result;
    }
#endif
}

GitManager::GitManager() { git_libgit2_init(); }
GitManager::~GitManager() { git_libgit2_shutdown(); }

std::string GitManager::lastError(const char *fallback)
{
    const git_error *error = git_error_last();
    return error && error->message ? error->message : fallback;
}

std::string GitManager::formatTime(git_time_t value, int offset_minutes)
{
    std::time_t adjusted = static_cast<std::time_t>(value + offset_minutes * 60);
    std::tm tm{};
#ifdef _WIN32
    gmtime_s(&tm, &adjusted);
#else
    gmtime_r(&adjusted, &tm);
#endif
    char buffer[48]{};
    std::strftime(buffer, sizeof(buffer), "%m/%d/%Y  %I:%M %p", &tm);
    return buffer;
}

void GitManager::readBranches(RepositoryView &repository, git_branch_t type,
                              std::vector<std::string> &output)
{
    git_branch_iterator *iterator = nullptr;
    if (git_branch_iterator_new(&iterator, repository.repo, type) != 0)
        return;
    git_reference *reference = nullptr;
    git_branch_t found{};
    while (git_branch_next(&reference, &found, iterator) == 0)
    {
        const char *name = nullptr;
        if (git_branch_name(&name, reference) == 0 && name)
        {
            if (type == GIT_BRANCH_REMOTE && std::string_view(name).ends_with("/HEAD"))
            {
                git_reference_free(reference);
                continue;
            }
            output.emplace_back(name);
        }
        git_reference_free(reference);
    }
    git_branch_iterator_free(iterator);
}

void GitManager::loadBranchDivergence(RepositoryView &repository)
{
    repository.local_branch_divergence.clear();
    repository.remote_branch_divergence.clear();
    for (const std::string &branch_name : repository.local_branches)
    {
        git_reference *local = nullptr;
        git_reference *upstream = nullptr;
        if (git_branch_lookup(&local, repository.repo, branch_name.c_str(), GIT_BRANCH_LOCAL) != 0 ||
            git_branch_upstream(&upstream, local) != 0)
        {
            git_reference_free(upstream);
            git_reference_free(local);
            continue;
        }
        const git_oid *local_oid = git_reference_target(local);
        const git_oid *upstream_oid = git_reference_target(upstream);
        size_t ahead = 0;
        size_t behind = 0;
        if (local_oid && upstream_oid &&
            git_graph_ahead_behind(&ahead, &behind, repository.repo, local_oid, upstream_oid) == 0)
        {
            repository.local_branch_divergence[branch_name] = {ahead, behind};
            const char *remote_name = nullptr;
            if (git_branch_name(&remote_name, upstream) == 0 && remote_name)
                repository.remote_branch_divergence[remote_name] = {behind, ahead};
        }
        git_reference_free(upstream);
        git_reference_free(local);
    }
}

void GitManager::loadRefBadges(RepositoryView &repository)
{
    for (CommitInfo &commit : repository.commits)
        commit.refs.clear();
    for (const git_branch_t type : {GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE})
    {
        git_branch_iterator *iterator = nullptr;
        if (git_branch_iterator_new(&iterator, repository.repo, type) != 0)
            continue;
        git_reference *reference = nullptr;
        git_branch_t found{};
        while (git_branch_next(&reference, &found, iterator) == 0)
        {
            const char *name = nullptr;
            if (git_branch_name(&name, reference) == 0 && name)
            {
                const std::string branch_name = name;
                if (type == GIT_BRANCH_REMOTE && branch_name.ends_with("/HEAD"))
                {
                    git_reference_free(reference);
                    continue;
                }
                RefBadge badge;
                badge.name = branch_name;
                badge.kind = type == GIT_BRANCH_LOCAL ? RefKind::local : RefKind::remote;
                badge.current = type == GIT_BRANCH_LOCAL && badge.name == repository.branch;
                badge.worktree = type == GIT_BRANCH_LOCAL &&
                                 (badge.current || repository.worktree_branches.contains(badge.name));
                if (type == GIT_BRANCH_LOCAL)
                {
                    git_reference *upstream = nullptr;
                    if (git_branch_upstream(&upstream, reference) == 0)
                    {
                        const git_oid *local_oid = git_reference_target(reference);
                        const git_oid *upstream_oid = git_reference_target(upstream);
                        // Collapse the cloud into the local badge only while both refs
                        // identify the same commit. A diverged remote gets its own badge.
                        badge.upstream = local_oid && upstream_oid &&
                                         git_oid_equal(local_oid, upstream_oid) != 0;
                    }
                    git_reference_free(upstream);
                }
                git_object *object = nullptr;
                if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0)
                {
                    const git_oid *oid = git_object_id(object);
                    if (type != GIT_BRANCH_REMOTE ||
                        !remoteIsCoveredByLocalUpstream(repository, oid, branch_name))
                        addBadge(repository, oid, std::move(badge));
                    git_object_free(object);
                }
            }
            git_reference_free(reference);
        }
        git_branch_iterator_free(iterator);
    }

    git_reference_iterator *tags = nullptr;
    if (git_reference_iterator_glob_new(&tags, repository.repo, "refs/tags/*") != 0)
        return;
    git_reference *reference = nullptr;
    while (git_reference_next(&reference, tags) == 0)
    {
        git_object *object = nullptr;
        if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0)
        {
            const char *name = git_reference_shorthand(reference);
            addBadge(repository, git_object_id(object), {name ? name : "tag", RefKind::tag});
            git_object_free(object);
        }
        git_reference_free(reference);
    }
    git_reference_iterator_free(tags);
}

void GitManager::computeGraphLanes(RepositoryView &repository)
{
    struct Lane
    {
        git_oid expected{};
    };

    const auto lane_for_commit = [](std::vector<Lane> &lanes, const git_oid &oid) {
        const auto found = std::find_if(lanes.begin(), lanes.end(), [&oid](const Lane &lane)
                                        { return git_oid_equal(&lane.expected, &oid) != 0; });
        if (found != lanes.end())
            return static_cast<size_t>(std::distance(lanes.begin(), found));
        lanes.push_back({oid});
        return lanes.size() - 1;
    };

    const auto contains_oid = [](const std::vector<Lane> &lanes, const git_oid &oid) {
        return std::any_of(lanes.begin(), lanes.end(), [&oid](const Lane &lane)
                           { return git_oid_equal(&lane.expected, &oid) != 0; });
    };

    std::vector<Lane> lanes;
    for (auto &commit : repository.commits)
    {
        size_t active_lane = lane_for_commit(lanes, commit.oid);
        const size_t commit_lane = active_lane;
        commit.lane = static_cast<int>(commit_lane);
        commit.graph_color = commit.lane;
        if (commit.parent_ids.empty())
        {
            lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(active_lane));
            continue;
        }

        lanes[active_lane].expected = commit.parent_ids.front();

        size_t insert_lane = active_lane + 1;
        for (size_t parent = 1; parent < commit.parent_ids.size(); ++parent)
        {
            if (contains_oid(lanes, commit.parent_ids[parent]))
                continue;
            lanes.insert(lanes.begin() + static_cast<std::ptrdiff_t>(std::min(insert_lane, lanes.size())),
                         {commit.parent_ids[parent]});
            ++insert_lane;
        }

        for (size_t duplicate = lanes.size(); duplicate-- > 0;)
        {
            if (duplicate == active_lane)
                continue;
            if (git_oid_equal(&lanes[duplicate].expected, &commit.oid) != 0)
            {
                lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(duplicate));
                if (duplicate < active_lane)
                    --active_lane;
            }
        }

        for (size_t left = 0; left < lanes.size(); ++left)
        {
            for (size_t right = lanes.size(); right-- > left + 1;)
            {
                if (git_oid_equal(&lanes[left].expected, &lanes[right].expected) != 0)
                    lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(right));
            }
        }
    }
}

void GitManager::loadWorkingTree(RepositoryView &repository)
{
    repository.working_files.clear();
    git_status_options options{};
    git_status_options_init(&options, GIT_STATUS_OPTIONS_VERSION);
    options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
    options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
                    GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR;
    git_status_list *list = nullptr;
    if (git_status_list_new(&list, repository.repo, &options) != 0)
        return;

    auto add_file = [&repository](const char *path, FileChangeKind kind, bool staged)
    {
        if (path)
            repository.working_files.push_back({path, kind, staged});
    };
    for (size_t index = 0; index < git_status_list_entrycount(list); ++index)
    {
        const git_status_entry *entry = git_status_byindex(list, index);
        if (!entry)
            continue;
        const char *index_path = entry->head_to_index && entry->head_to_index->new_file.path
                                     ? entry->head_to_index->new_file.path
                                     : nullptr;
        const char *worktree_path = entry->index_to_workdir && entry->index_to_workdir->new_file.path
                                        ? entry->index_to_workdir->new_file.path
                                        : nullptr;
        if (entry->status & GIT_STATUS_INDEX_NEW)
            add_file(index_path, FileChangeKind::added, true);
        else if (entry->status & GIT_STATUS_INDEX_DELETED)
            add_file(index_path, FileChangeKind::deleted, true);
        else if (entry->status & GIT_STATUS_INDEX_RENAMED)
            add_file(index_path, FileChangeKind::renamed, true);
        else if (entry->status & GIT_STATUS_INDEX_MODIFIED)
            add_file(index_path, FileChangeKind::modified, true);

        if (entry->status & GIT_STATUS_WT_NEW)
            add_file(worktree_path, FileChangeKind::added, false);
        else if (entry->status & GIT_STATUS_WT_DELETED)
            add_file(worktree_path, FileChangeKind::deleted, false);
        else if (entry->status & GIT_STATUS_WT_RENAMED)
            add_file(worktree_path, FileChangeKind::renamed, false);
        else if (entry->status & GIT_STATUS_WT_MODIFIED)
            add_file(worktree_path, FileChangeKind::modified, false);
    }
    git_status_list_free(list);
}

void GitManager::loadToolbarHistoryActions(RepositoryView &repository)
{
    repository.undo_action = unavailableAction("No recent action found to undo");
    repository.redo_action = unavailableAction("No recent undo found to redo");

    std::string output;
    std::string error;
    if (!captureGit(repository, {"reflog", "--format=%gs", "-n", "2", "HEAD"}, output, error))
        return;

    std::vector<std::string> entries;
    std::istringstream stream(output);
    std::string line;
    while (std::getline(stream, line))
    {
        if (!line.empty() && line.back() == '\r')
            line.pop_back();
        if (!line.empty())
            entries.push_back(std::move(line));
    }
    if (entries.empty())
        return;

    std::string from;
    std::string to;
    if (parseCheckoutReflog(entries[0], from, to))
    {
        repository.undo_action = makeCheckoutAction(from, to, "Undo");
        return;
    }

    if (startsWith(entries[0], "commit"))
    {
        repository.undo_action = makeCommitAction(commitSummaryFromReflog(entries[0]), "Undo");
        return;
    }

    if (startsWith(entries[0], "reset: moving to HEAD~1") && entries.size() > 1 &&
        startsWith(entries[1], "commit"))
    {
        repository.redo_action = makeCommitAction(commitSummaryFromReflog(entries[1]), "Redo");
    }
}

bool GitManager::loadRepositoryData(RepositoryView &repository, std::string &error)
{
    repository.local_branches.clear();
    repository.remote_branches.clear();
    repository.local_branch_divergence.clear();
    repository.remote_branch_divergence.clear();
    repository.remotes.clear();
    repository.worktrees.clear();
    repository.worktree_branches.clear();
    repository.submodules.clear();
    repository.submodule_statuses.clear();
    repository.commits.clear();
    repository.selected_commit = 0;
    repository.scroll_to_commit = -1;
    repository.undo_action = {};
    repository.redo_action = {};
    if (repository.commit_walk)
        git_revwalk_free(repository.commit_walk);
    repository.commit_walk = nullptr;
    repository.history_exhausted = false;
    loadWorkingTree(repository);
    populateRepositoryIdentity(repository);
    loadToolbarHistoryActions(repository);
    readBranches(repository, GIT_BRANCH_LOCAL, repository.local_branches);
    readBranches(repository, GIT_BRANCH_REMOTE, repository.remote_branches);
    loadBranchDivergence(repository);

    git_strarray names{};
    if (git_remote_list(&names, repository.repo) == 0)
    {
        for (size_t i = 0; i < names.count; ++i)
            repository.remotes.emplace_back(names.strings[i]);
        git_strarray_dispose(&names);
    }
    if (git_worktree_list(&names, repository.repo) == 0)
    {
        for (size_t i = 0; i < names.count; ++i)
        {
            repository.worktrees.emplace_back(names.strings[i]);
            git_worktree *worktree = nullptr;
            git_repository *worktree_repository = nullptr;
            git_reference *worktree_head = nullptr;
            if (git_worktree_lookup(&worktree, repository.repo, names.strings[i]) == 0 &&
                git_repository_open_from_worktree(&worktree_repository, worktree) == 0 &&
                git_repository_head(&worktree_head, worktree_repository) == 0)
            {
                if (const char *branch = git_reference_shorthand(worktree_head))
                    repository.worktree_branches.emplace(branch);
            }
            git_reference_free(worktree_head);
            git_repository_free(worktree_repository);
            git_worktree_free(worktree);
        }
        git_strarray_dispose(&names);
    }
    git_submodule_foreach(repository.repo, submoduleCallback, &repository);

    if (git_revwalk_new(&repository.commit_walk, repository.repo) != 0)
    {
        error = lastError("Unable to read history");
        return false;
    }
    git_revwalk_sorting(repository.commit_walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
    bool has_history = false;
    for (const char *glob : {"refs/heads/*", "refs/remotes/*", "refs/tags/*"})
    {
        const int push_result = git_revwalk_push_glob(repository.commit_walk, glob);
        if (push_result == 0)
            has_history = true;
    }
    if (!has_history)
    {
        git_revwalk_free(repository.commit_walk);
        repository.commit_walk = nullptr;
        repository.history_exhausted = true;
        return true;
    }
    return loadMoreCommits(repository, 500, error);
}

bool GitManager::loadMoreCommits(RepositoryView &repository, size_t page_size, std::string &error)
{
    if (!repository.commit_walk || repository.history_exhausted || page_size == 0)
        return true;
    size_t loaded = 0;
    git_oid oid{};
    while (loaded < page_size)
    {
        const int walk_result = git_revwalk_next(&oid, repository.commit_walk);
        if (walk_result == GIT_ITEROVER)
        {
            repository.history_exhausted = true;
            git_revwalk_free(repository.commit_walk);
            repository.commit_walk = nullptr;
            break;
        }
        if (walk_result != 0)
        {
            error = lastError("Unable to continue reading history");
            return false;
        }
        git_commit *commit = nullptr;
        if (git_commit_lookup(&commit, repository.repo, &oid) != 0)
            continue;
        CommitInfo item;
        item.oid = oid;
        char id[GIT_OID_SHA1_HEXSIZE + 1]{};
        git_oid_tostr(id, sizeof(id), &oid);
        item.short_id.assign(id, 8);
        item.summary = git_commit_summary(commit) ? git_commit_summary(commit) : "(no commit message)";
        if (const char *body = git_commit_body(commit))
        {
            bool spacing = false;
            for (const unsigned char character : std::string_view(body))
            {
                if (std::isspace(character))
                {
                    spacing = !item.description.empty();
                }
                else
                {
                    if (spacing)
                        item.description.push_back(' ');
                    item.description.push_back(static_cast<char>(character));
                    spacing = false;
                }
            }
        }
        if (const git_signature *author = git_commit_author(commit))
        {
            item.author = author->name ? author->name : "Unknown";
            item.email = author->email ? author->email : "";
            item.date = formatTime(author->when.time, author->when.offset);
        }
        item.parents = static_cast<int>(git_commit_parentcount(commit));
        for (size_t i = 0; i < git_commit_parentcount(commit); ++i)
            if (const git_oid *parent = git_commit_parent_id(commit, static_cast<unsigned int>(i)))
                item.parent_ids.push_back(*parent);
        repository.commits.push_back(std::move(item));
        git_commit_free(commit);
        ++loaded;
    }
    computeGraphLanes(repository);
    loadRefBadges(repository);
    return true;
}

bool GitManager::openRepositoryHandle(RepositoryView &repository, const std::string &path, std::string &error)
{
    git_repository *opened = nullptr;
    if (git_repository_open_ext(&opened, path.c_str(), 0, nullptr) != 0)
    {
        error = lastError("Unable to open repository");
        return false;
    }
    repository.close();
    repository.repo = opened;
    populateRepositoryIdentity(repository);
    return true;
}

bool GitManager::openRepository(RepositoryView &repository, const std::string &path, std::string &error)
{
    if (!openRepositoryHandle(repository, path, error))
        return false;
    return loadRepositoryData(repository, error);
}

bool GitManager::initializeRepository(RepositoryView &repository, const std::string &path,
                                      const std::string &initial_branch, std::string &error)
{
    git_repository *created = nullptr;
    git_repository_init_options options{};
    git_repository_init_options_init(&options, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
    options.flags = GIT_REPOSITORY_INIT_MKPATH;
    options.initial_head = initial_branch.empty() ? "main" : initial_branch.c_str();
    if (git_repository_init_ext(&created, path.c_str(), &options) != 0)
    {
        error = lastError("Unable to initialize repository");
        return false;
    }
    git_repository_free(created);
    return openRepository(repository, path, error);
}

bool GitManager::cloneRepository(RepositoryView &repository, const std::string &url,
                                 const std::string &path, bool shallow, std::string &error)
{
    git_clone_options options{};
    git_clone_options_init(&options, GIT_CLONE_OPTIONS_VERSION);
    if (shallow)
        options.fetch_opts.depth = 1;
    git_repository *cloned = nullptr;
    if (git_clone(&cloned, url.c_str(), path.c_str(), &options) != 0)
    {
        error = lastError("Unable to clone repository");
        return false;
    }
    repository.close();
    repository.repo = cloned;
    return loadRepositoryData(repository, error);
}

bool GitManager::reload(RepositoryView &repository, std::string &error)
{
    return repository.repo && loadRepositoryData(repository, error);
}

bool GitManager::runGit(RepositoryView &repository, const std::vector<std::string> &arguments,
                        const char *success_message, std::string &message, bool reload_repository,
                        const std::optional<GitAuthOverride> &auth)
{
    std::string command_output;
    if (!captureGit(repository, withAuthOverrideArguments(arguments, auth), command_output, message))
        return false;
    if (reload_repository)
    {
        std::string reload_error;
        if (!loadRepositoryData(repository, reload_error))
        {
            message = reload_error;
            return false;
        }
    }
    message = success_message;
    return true;
}

bool GitManager::captureGit(RepositoryView &repository, const std::vector<std::string> &arguments,
                            std::string &command_output, std::string &error,
                            const std::string *stdin_data)
{
    if (!repository.repo)
    {
        error = "No repository is open";
        return false;
    }
#ifdef _WIN32
    wchar_t temporary_directory[MAX_PATH]{};
    wchar_t output_path[MAX_PATH]{};
    if (!GetTempPathW(MAX_PATH, temporary_directory) ||
        !GetTempFileNameW(temporary_directory, L"gtr", 0, output_path))
    {
        error = "Unable to create Git command output file";
        return false;
    }
    SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
    HANDLE input_read = nullptr;
    HANDLE input_write = nullptr;
    if (stdin_data && !CreatePipe(&input_read, &input_write, &security, 0))
    {
        DeleteFileW(output_path);
        error = "Unable to create Git command input pipe";
        return false;
    }
    if (input_write)
        SetHandleInformation(input_write, HANDLE_FLAG_INHERIT, 0);
    HANDLE output = CreateFileW(output_path, GENERIC_READ | GENERIC_WRITE,
                                FILE_SHARE_READ | FILE_SHARE_WRITE, &security, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, nullptr);
    if (output == INVALID_HANDLE_VALUE)
    {
        if (input_read)
            CloseHandle(input_read);
        if (input_write)
            CloseHandle(input_write);
        DeleteFileW(output_path);
        error = "Unable to capture Git command output";
        return false;
    }

    std::wstring command = L"git -C " + quoteWindowsArgument(utf8ToWide(repository.path));
    for (const std::string &argument : arguments)
        command += L" " + quoteWindowsArgument(utf8ToWide(argument));
    std::vector<wchar_t> mutable_command(command.begin(), command.end());
    mutable_command.push_back(L'\0');

    STARTUPINFOW startup{};
    startup.cb = sizeof(startup);
    startup.dwFlags = STARTF_USESTDHANDLES;
    startup.hStdOutput = output;
    startup.hStdError = output;
    startup.hStdInput = input_read ? input_read : GetStdHandle(STD_INPUT_HANDLE);
    PROCESS_INFORMATION process{};
    const BOOL started = CreateProcessW(nullptr, mutable_command.data(), nullptr, nullptr, TRUE,
                                        CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process);
    DWORD exit_code = 1;
    if (started)
    {
        if (input_read)
        {
            CloseHandle(input_read);
            input_read = nullptr;
        }
        if (input_write && stdin_data)
        {
            DWORD bytes_written = 0;
            WriteFile(input_write, stdin_data->data(), static_cast<DWORD>(stdin_data->size()), &bytes_written, nullptr);
            CloseHandle(input_write);
            input_write = nullptr;
        }
        WaitForSingleObject(process.hProcess, INFINITE);
        GetExitCodeProcess(process.hProcess, &exit_code);
        CloseHandle(process.hThread);
        CloseHandle(process.hProcess);
    }
    if (input_read)
        CloseHandle(input_read);
    if (input_write)
        CloseHandle(input_write);
    FlushFileBuffers(output);
    SetFilePointer(output, 0, nullptr, FILE_BEGIN);
    command_output.clear();
    char buffer[4096];
    DWORD bytes_read = 0;
    while (ReadFile(output, buffer, sizeof(buffer), &bytes_read, nullptr) && bytes_read)
        command_output.append(buffer, bytes_read);
    CloseHandle(output);
    DeleteFileW(output_path);
    while (!command_output.empty() && (command_output.back() == '\r' || command_output.back() == '\n'))
        command_output.pop_back();
    if (!started || exit_code != 0)
    {
        error = command_output.empty() ? "Unable to run Git command" : command_output;
        return false;
    }
#else
    (void)arguments;
    error = "Git commands are currently supported on Windows";
    return false;
#endif
    return true;
}

bool GitManager::prepareCredentials(RepositoryView &repository, std::string &error)
{
    if (repository.credentials_checked)
        return true;

    bool uses_https = false;
    for (const std::string &remote_name : repositoryRemotes(repository.repo))
    {
        git_remote *remote = nullptr;
        if (git_remote_lookup(&remote, repository.repo, remote_name.c_str()) == 0)
        {
            const char *url = git_remote_url(remote);
            const std::string remote_url = url ? url : "";
            uses_https = uses_https || remote_url.starts_with("https://") || remote_url.starts_with("http://");
            git_remote_free(remote);
        }
    }
    if (!uses_https)
    {
        repository.credentials_checked = true;
        return true;
    }

    std::string helper;
    std::string helper_error;
    const bool has_helper = captureGit(repository, {"config", "--get-all", "credential.helper"},
                                       helper, helper_error) &&
                            !helper.empty();

    std::string gcm_version;
    std::string gcm_error;
    const bool gcm_available = captureGit(repository, {"credential-manager", "--version"},
                                          gcm_version, gcm_error);

    if (has_helper)
    {
        if (helper.find("manager") != std::string::npos && !gcm_available)
        {
            error = "Git Credential Manager is configured but unavailable. Reinstall Git for Windows with GCM enabled.";
            return false;
        }
        repository.credentials_checked = true;
        return true;
    }

    if (!gcm_available)
    {
        repository.credentials_checked = true;
        return true;
    }

    std::string configure_output;
    if (!captureGit(repository, {"config", "--local", "credential.helper", "manager"},
                    configure_output, error))
        return false;
    repository.credentials_checked = true;
    return true;
}

std::string GitManager::remoteUrl(RepositoryView &repository, const std::string &remote_name, std::string &error)
{
    git_remote *remote = nullptr;
    if (git_remote_lookup(&remote, repository.repo, remote_name.c_str()) != 0)
    {
        error = lastError("Unable to find remote");
        return {};
    }
    const char *url = git_remote_url(remote);
    const std::string remote_url = url ? url : "";
    git_remote_free(remote);
    if (remote_url.empty())
        error = "Remote has no URL";
    return remote_url;
}

bool GitManager::storeCredential(RepositoryView &repository, const std::string &remote_url,
                                 const std::string &username, const std::string &password,
                                 std::string &error)
{
    if (username.empty())
    {
        error = "Username is required";
        return false;
    }
    if (!prepareCredentials(repository, error))
        return false;
    std::string helper;
    std::string helper_error;
    if (!captureGit(repository, {"config", "--get-all", "credential.helper"}, helper, helper_error) || helper.empty())
    {
        error = helper_error.empty() ? "No Git credential helper is configured" : helper_error;
        return false;
    }
    const size_t scheme = remote_url.find("://");
    if (scheme == std::string::npos)
    {
        error = "Remote URL is invalid";
        return false;
    }
    const size_t authority_begin = scheme + 3;
    const size_t path_begin = remote_url.find('/', authority_begin);
    const std::string protocol = remote_url.substr(0, scheme);
    const std::string host = path_begin == std::string::npos
                                 ? remote_url.substr(authority_begin)
                                 : remote_url.substr(authority_begin, path_begin - authority_begin);
    const std::string path = path_begin == std::string::npos ? "/" : remote_url.substr(path_begin);
    std::ostringstream input;
    input << "protocol=" << protocol << "\n";
    input << "host=" << host << "\n";
    input << "path=" << path << "\n";
    input << "username=" << username << "\n";
    input << "password=" << password << "\n\n";
    std::string output;
    std::string payload = input.str();
    return captureGit(repository, {"credential", "approve"}, output, error, &payload);
}

bool GitManager::isAuthenticationFailure(std::string_view message)
{
    std::string lowered(message);
    std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char value)
                   { return static_cast<char>(std::tolower(value)); });
    return lowered.find(" 403") != std::string::npos ||
           lowered.find(" 401") != std::string::npos ||
           lowered.find("forbidden") != std::string::npos ||
           lowered.find("authentication failed") != std::string::npos ||
           lowered.find("access denied") != std::string::npos ||
           lowered.find("could not read username") != std::string::npos ||
           lowered.find("terminal prompts disabled") != std::string::npos;
}

bool GitManager::applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
                            bool reverse, std::string &error)
{
    std::filesystem::path temporary_directory = izo::GetKnownPath(izo::KnownPath::Temporary);
    if (temporary_directory.empty())
        temporary_directory = std::filesystem::temp_directory_path();
    const std::filesystem::path patch_path = temporary_directory /
                                             ("gitree-hunk-" + std::to_string(izo::MonotonicNow().time_since_epoch().count()) + ".patch");
    {
        std::ofstream stream(patch_path, std::ios::binary);
        if (!stream)
        {
            error = "Unable to create temporary patch";
            return false;
        }
        stream.write(patch.data(), static_cast<std::streamsize>(patch.size()));
    }
    std::vector<std::string> arguments{"apply", "--whitespace=nowarn"};
    if (cached)
        arguments.push_back("--cached");
    if (reverse)
        arguments.push_back("--reverse");
    arguments.push_back(patch_path.string());
    const bool applied = runGit(repository, arguments,
                                reverse ? "Hunk reverted" : "Hunk staged", error);
    std::error_code remove_error;
    std::filesystem::remove(patch_path, remove_error);
    return applied;
}

bool GitManager::fetch(RepositoryView &repository, const std::string &remote, std::string &error,
                       const std::optional<GitAuthOverride> &auth)
{
    if (!prepareCredentials(repository, error))
        return false;
    const std::vector<std::string> args = remote.empty()
                                              ? std::vector<std::string>{"fetch", "--all", "--prune"}
                                              : std::vector<std::string>{"fetch", remote, "--prune"};
    return runGit(repository, args, "Fetch complete", error, true, auth);
}

bool GitManager::pull(RepositoryView &repository, int mode, std::string &error,
                      const std::optional<GitAuthOverride> &auth)
{
    if (!prepareCredentials(repository, error))
        return false;
    if (mode == 0)
        return fetch(repository, {}, error, auth);
    std::vector<std::string> args{"pull"};
    if (mode == 1)
        args.push_back("--ff");
    else if (mode == 2)
        args.push_back("--ff-only");
    else if (mode == 3)
        args.push_back("--rebase");
    return runGit(repository, args, "Pull complete", error, true, auth);
}

bool GitManager::push(RepositoryView &repository, std::string &error,
                      const std::optional<GitAuthOverride> &auth)
{
    if (!prepareCredentials(repository, error))
        return false;
    if (runGit(repository, {"push"}, "Push complete", error, true, auth))
        return true;
    const std::string remote = preferredRemoteName(repository.repo);
    if (remote.empty())
        return false;
    return runGit(repository, {"push", "--set-upstream", remote, repository.branch},
                  "Push complete; upstream configured", error, true, auth);
}

bool GitManager::pushBranch(RepositoryView &repository, const std::string &branch, std::string &error,
                            const std::optional<GitAuthOverride> &auth)
{
    if (!prepareCredentials(repository, error))
        return false;

    git_reference *reference = nullptr;
    if (git_branch_lookup(&reference, repository.repo, branch.c_str(), GIT_BRANCH_LOCAL) != 0)
    {
        error = lastError("Unable to find branch");
        return false;
    }

    std::string remote_name;
    std::string remote_branch_name;
    git_reference *upstream = nullptr;
    if (git_branch_upstream(&upstream, reference) == 0)
    {
        const char *upstream_name = nullptr;
        if (git_branch_name(&upstream_name, upstream) == 0 && upstream_name)
        {
            remote_branch_name = upstream_name;
            const size_t slash = remote_branch_name.find('/');
            remote_name = slash == std::string::npos ? remote_branch_name : remote_branch_name.substr(0, slash);
            if (slash != std::string::npos)
                remote_branch_name.erase(0, slash + 1);
        }
        git_reference_free(upstream);
    }
    git_reference_free(reference);

    if (!remote_name.empty() && !remote_branch_name.empty())
        return runGit(repository, {"push", remote_name, branch + ":" + remote_branch_name},
                      "Push complete", error, true, auth);

    const std::string remote = preferredRemoteName(repository.repo);
    if (remote.empty())
    {
        error = "No remote is configured for this repository";
        return false;
    }
    return runGit(repository, {"push", "--set-upstream", remote, branch + ":" + branch},
                  "Push complete; upstream configured", error, true, auth);
}

bool GitManager::stash(RepositoryView &repository, std::string &error)
{
    return runGit(repository, {"stash", "push", "--include-untracked", "-m", "Gitree stash"},
                  "Changes stashed", error);
}

bool GitManager::popStash(RepositoryView &repository, std::string &error)
{
    return runGit(repository, {"stash", "pop"}, "Stash applied", error);
}

bool GitManager::undoCommit(RepositoryView &repository, std::string &error)
{
    if (!repository.undo_action.available)
    {
        error = repository.undo_action.tooltip.empty() ? "No recent action found to undo" : repository.undo_action.tooltip;
        return false;
    }

    std::vector<std::string> arguments;
    std::string success_message;
    ToolbarHistoryAction redo_action = unavailableAction("No recent undo found to redo");
    switch (repository.undo_action.kind)
    {
    case ToolbarHistoryActionKind::checkout:
        arguments = {"checkout", repository.undo_action.source};
        success_message = "Checked out " + repository.undo_action.source;
        redo_action = makeCheckoutAction(repository.undo_action.source, repository.undo_action.target, "Redo");
        break;
    case ToolbarHistoryActionKind::commit:
        arguments = {"reset", "--soft", "HEAD~1"};
        success_message = "Last commit moved back to staging";
        redo_action = makeCommitAction(repository.undo_action.summary, "Redo");
        break;
    default:
        error = "No supported action is available to undo";
        return false;
    }

    std::string command_output;
    if (!captureGit(repository, arguments, command_output, error))
        return false;
    if (!loadRepositoryData(repository, error))
        return false;
    repository.redo_action = std::move(redo_action);
    error = success_message;
    return true;
}

bool GitManager::redoCommit(RepositoryView &repository, std::string &error)
{
    if (!repository.redo_action.available)
    {
        error = repository.redo_action.tooltip.empty() ? "No recent undo found to redo" : repository.redo_action.tooltip;
        return false;
    }

    std::vector<std::string> arguments;
    std::string success_message;
    switch (repository.redo_action.kind)
    {
    case ToolbarHistoryActionKind::checkout:
        arguments = {"checkout", repository.redo_action.target};
        success_message = "Checked out " + repository.redo_action.target;
        break;
    case ToolbarHistoryActionKind::commit:
        arguments = {"reset", "--soft", "HEAD@{1}"};
        success_message = "Commit restored from reflog";
        break;
    default:
        error = "No supported action is available to redo";
        return false;
    }

    std::string command_output;
    if (!captureGit(repository, arguments, command_output, error))
        return false;
    if (!loadRepositoryData(repository, error))
        return false;
    error = success_message;
    return true;
}

bool GitManager::stageAll(RepositoryView &repository, std::string &error)
{
    if (!runGit(repository, {"add", "--all"}, "All changes staged", error, false))
        return false;
    loadWorkingTree(repository);
    return true;
}

bool GitManager::unstageAll(RepositoryView &repository, std::string &error)
{
    if (git_repository_head_unborn(repository.repo) == 1)
    {
        if (!runGit(repository, {"rm", "--cached", "-r", "--ignore-unmatch", "--", "."},
                      "All changes unstaged", error, false))
            return false;
        loadWorkingTree(repository);
        return true;
    }
    if (runGit(repository, {"restore", "--staged", "--", "."}, "All changes unstaged", error, false))
    {
        loadWorkingTree(repository);
        return true;
    }
    if (!runGit(repository, {"reset", "HEAD", "--", "."}, "All changes unstaged", error, false))
        return false;
    loadWorkingTree(repository);
    return true;
}

bool GitManager::stageFile(RepositoryView &repository, const std::string &path, std::string &error)
{
    return runGit(repository, {"add", "--", path}, "File staged", error);
}

bool GitManager::unstageFile(RepositoryView &repository, const std::string &path, std::string &error)
{
    if (git_repository_head_unborn(repository.repo) == 1)
        return runGit(repository, {"rm", "--cached", "--ignore-unmatch", "--", path},
                      "File unstaged", error);
    if (runGit(repository, {"restore", "--staged", "--", path}, "File unstaged", error))
        return true;
    return runGit(repository, {"reset", "HEAD", "--", path}, "File unstaged", error);
}

bool GitManager::discardFile(RepositoryView &repository, const std::string &path, std::string &error)
{
    if (runGit(repository, {"restore", "--worktree", "--", path}, "File changes discarded", error))
        return true;
    return runGit(repository, {"clean", "-f", "--", path}, "Untracked file removed", error);
}

bool GitManager::discardAll(RepositoryView &repository, std::string &error)
{
    if (!runGit(repository, {"reset", "--hard", "HEAD"}, "Tracked changes discarded", error, false))
        return false;
    return runGit(repository, {"clean", "-fd"}, "All working changes discarded", error);
}

bool GitManager::commit(RepositoryView &repository, const std::string &summary,
                        const std::string &description, bool amend, std::string &error)
{
    std::vector<std::string> args{"commit"};
    if (amend)
        args.push_back("--amend");
    args.insert(args.end(), {"-m", summary});
    if (!description.empty())
        args.insert(args.end(), {"-m", description});
    return runGit(repository, args, amend ? "Commit amended" : "Commit created", error);
}

bool GitManager::createBranch(RepositoryView &repository, const std::string &name,
                              const std::string &start_point, bool checkout, std::string &error)
{
    std::vector<std::string> args{checkout ? "switch" : "branch"};
    if (checkout)
        args.insert(args.end(), {"-c", name});
    else
        args.push_back(name);
    if (!start_point.empty())
        args.push_back(start_point);
    return runGit(repository, args, checkout ? "Branch created and checked out" : "Branch created", error);
}

bool GitManager::createTag(RepositoryView &repository, const std::string &name,
                           const std::string &target, std::string &error)
{
    std::vector<std::string> args{"tag", name};
    if (!target.empty())
        args.push_back(target);
    return runGit(repository, args, "Tag created", error);
}

bool GitManager::addRemote(RepositoryView &repository, const std::string &name,
                           const std::string &url, std::string &error)
{
    repository.credentials_checked = false;
    return runGit(repository, {"remote", "add", name, url}, "Remote added", error);
}

bool GitManager::addWorktree(RepositoryView &repository, const std::string &path,
                             const std::string &branch, std::string &error)
{
    std::vector<std::string> args{"worktree", "add", path};
    if (!branch.empty())
        args.push_back(branch);
    return runGit(repository, args, "Worktree added", error);
}

bool GitManager::addSubmodule(RepositoryView &repository, const std::string &url,
                              const std::string &path, std::string &error)
{
    std::vector<std::string> args{"submodule", "add", url};
    if (!path.empty())
        args.push_back(path);
    return runGit(repository, args, "Submodule added", error);
}

bool GitManager::checkoutBranch(RepositoryView &repository, const std::string &branch, std::string &error)
{
    git_reference *reference = nullptr;
    if (git_branch_lookup(&reference, repository.repo, branch.c_str(), GIT_BRANCH_LOCAL) != 0)
    {
        error = lastError("Unable to find branch");
        return false;
    }
    const git_oid *reference_target = git_reference_target(reference);
    git_oid target{};
    const bool has_target = reference_target != nullptr;
    if (has_target)
        git_oid_cpy(&target, reference_target);
    const char *reference_name = git_reference_name(reference);
    const int set_head_result = git_repository_set_head(repository.repo, reference_name);
    git_reference_free(reference);
    if (set_head_result != 0)
    {
        error = lastError("Unable to switch HEAD");
        return false;
    }
    git_checkout_options options{};
    git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION);
    options.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING;
    if (git_checkout_head(repository.repo, &options) != 0)
    {
        error = lastError("Unable to checkout branch");
        return false;
    }
    if (!loadRepositoryData(repository, error))
        return false;

    auto find_target = [&]() {
        return std::find_if(repository.commits.begin(), repository.commits.end(), [&](const CommitInfo &commit) {
            return has_target && git_oid_equal(&commit.oid, &target) != 0;
        });
    };
    auto target_commit = find_target();
    while (has_target && target_commit == repository.commits.end() && !repository.history_exhausted)
    {
        if (!loadMoreCommits(repository, 500, error))
            return false;
        target_commit = find_target();
    }
    if (target_commit != repository.commits.end())
    {
        repository.selected_commit = static_cast<int>(std::distance(repository.commits.begin(), target_commit));
        repository.scroll_to_commit = repository.selected_commit;
    }
    return true;
}

bool GitManager::mergeBranch(RepositoryView &repository, const std::string &source_branch,
                             const std::string &target_branch, std::string &error)
{
    if (source_branch.empty() || target_branch.empty())
    {
        error = "Unable to merge branches";
        return false;
    }
    if (source_branch == target_branch)
    {
        error = "A branch cannot be merged into itself";
        return false;
    }
    if (repository.branch != target_branch && !checkoutBranch(repository, target_branch, error))
        return false;
    if (!runGit(repository, {"merge", "--no-ff", "--no-edit", source_branch}, "Branches merged", error))
        return false;
    error = "Merged " + source_branch + " into " + target_branch;
    return true;
}

bool GitManager::updateSubmodule(RepositoryView &repository, const std::string &name, std::string &error)
{
    git_submodule *submodule = nullptr;
    if (git_submodule_lookup(&submodule, repository.repo, name.c_str()) != 0)
    {
        error = lastError("Unable to find submodule");
        return false;
    }
    git_submodule_update_options options{};
    git_submodule_update_options_init(&options, GIT_SUBMODULE_UPDATE_OPTIONS_VERSION);
    options.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING;
    const int result = git_submodule_update(submodule, 1, &options);
    git_submodule_free(submodule);
    if (result != 0)
    {
        error = lastError("Unable to update submodule");
        return false;
    }
    error = "Updated submodule " + name;
    return loadRepositoryData(repository, error);
}

std::string GitManager::worktreePath(RepositoryView &repository, const std::string &name, std::string &error)
{
    git_worktree *worktree = nullptr;
    if (git_worktree_lookup(&worktree, repository.repo, name.c_str()) != 0)
    {
        error = lastError("Unable to find worktree");
        return {};
    }
    const char *path = git_worktree_path(worktree);
    std::string result = path ? path : "";
    git_worktree_free(worktree);
    return result;
}

bool GitManager::loadCommitChanges(RepositoryView &repository, int commit_index, std::string &error)
{
    if (commit_index < 0 || commit_index >= static_cast<int>(repository.commits.size()))
        return false;
    CommitInfo &info = repository.commits[commit_index];
    if (info.changes_loaded)
        return true;

    git_commit *commit = nullptr;
    git_commit *parent = nullptr;
    git_tree *tree = nullptr;
    git_tree *parent_tree = nullptr;
    git_diff *diff = nullptr;
    bool success = false;
    if (git_commit_lookup(&commit, repository.repo, &info.oid) == 0 &&
        git_commit_tree(&tree, commit) == 0)
    {
        if (git_commit_parentcount(commit) > 0 && git_commit_parent(&parent, commit, 0) == 0)
            git_commit_tree(&parent_tree, parent);
        git_diff_options options{};
        git_diff_options_init(&options, GIT_DIFF_OPTIONS_VERSION);
        if (git_diff_tree_to_tree(&diff, repository.repo, parent_tree, tree, &options) == 0 &&
            git_diff_foreach(diff, diffFileCallback, nullptr, nullptr, nullptr, &info.changed_files) == 0)
        {
            info.changes_loaded = true;
            success = true;
        }
    }
    if (!success)
        error = lastError("Unable to load changed files");
    git_diff_free(diff);
    git_tree_free(parent_tree);
    git_tree_free(tree);
    git_commit_free(parent);
    git_commit_free(commit);
    return success;
}

bool GitManager::loadCommitFiles(RepositoryView &repository, int commit_index, std::string &error)
{
    if (commit_index < 0 || commit_index >= static_cast<int>(repository.commits.size()))
        return false;
    CommitInfo &info = repository.commits[commit_index];
    if (info.all_files_loaded)
        return true;
    if (!loadCommitChanges(repository, commit_index, error))
        return false;

    git_commit *commit = nullptr;
    git_tree *tree = nullptr;
    bool success = false;
    if (git_commit_lookup(&commit, repository.repo, &info.oid) == 0 &&
        git_commit_tree(&tree, commit) == 0 &&
        git_tree_walk(tree, GIT_TREEWALK_PRE, treeFileCallback, &info.all_files) == 0)
    {
        for (ChangedFile &file : info.all_files)
        {
            const auto changed = std::find_if(info.changed_files.begin(), info.changed_files.end(),
                                              [&file](const ChangedFile &candidate)
                                              { return candidate.path == file.path; });
            if (changed != info.changed_files.end())
                file.kind = changed->kind;
        }
        for (const ChangedFile &changed : info.changed_files)
        {
            const bool present = std::any_of(info.all_files.begin(), info.all_files.end(),
                                             [&changed](const ChangedFile &file)
                                             { return file.path == changed.path; });
            if (!present)
                info.all_files.push_back(changed);
        }
        std::sort(info.all_files.begin(), info.all_files.end(),
                  [](const ChangedFile &left, const ChangedFile &right)
                  { return left.path < right.path; });
        info.all_files_loaded = true;
        success = true;
    }
    if (!success)
    {
        info.all_files.clear();
        error = lastError("Unable to load repository files");
    }
    git_tree_free(tree);
    git_commit_free(commit);
    return success;
}
