diff --git a/src/managers/git_manager.cpp b/src/managers/git_manager.cpp index c2a9395..c804dbf 100644 --- a/src/managers/git_manager.cpp +++ b/src/managers/git_manager.cpp @@ -212,3 +212,57 @@ bool GitManager::initializeRepository(RepositoryView& repository, const std::str bool GitManager::reload(RepositoryView& repository, std::string& error) { return repository.repo && loadRepositoryData(repository, 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 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; + } + return loadRepositoryData(repository, error); +} + +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; +} diff --git a/src/managers/git_manager.h b/src/managers/git_manager.h index b57a9c4..2024a76 100644 --- a/src/managers/git_manager.h +++ b/src/managers/git_manager.h @@ -11,6 +11,9 @@ public: bool openRepository(RepositoryView& repository, const std::string& path, std::string& error); bool initializeRepository(RepositoryView& repository, const std::string& path, std::string& error); bool reload(RepositoryView& repository, std::string& error); + bool checkoutBranch(RepositoryView& repository, const std::string& branch, std::string& error); + bool updateSubmodule(RepositoryView& repository, const std::string& name, std::string& error); + std::string worktreePath(RepositoryView& repository, const std::string& name, std::string& error); private: static std::string lastError(const char* fallback); diff --git a/src/ui/gitree_ui.cpp b/src/ui/gitree_ui.cpp index f32817c..fa0c2b8 100644 --- a/src/ui/gitree_ui.cpp +++ b/src/ui/gitree_ui.cpp @@ -180,12 +180,31 @@ bool sidebar_section_header(const char* label, int count, const char* add_toolti return open; } +enum class SidebarItemKind { worktree, submodule }; + +void sidebar_item_context(const std::string& item, SidebarItemKind kind) { + if (!ImGui::BeginPopupContextItem()) return; + if (kind == SidebarItemKind::submodule && ImGui::MenuItem(ICON_FA_DOWNLOAD " Pull / Update")) + g_git_manager->updateSubmodule(repo(), item, g_notice); + if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Open in file manager")) { + std::string error; + const std::filesystem::path path = kind == SidebarItemKind::worktree + ? g_git_manager->worktreePath(repo(), item, error) + : std::filesystem::path(repo().path) / item; + if (path.empty() || !izo::open_path(path, &error)) g_notice = error; + } + ImGui::Separator(); + if (ImGui::MenuItem(ICON_FA_COPY " Copy name")) ImGui::SetClipboardText(item.c_str()); + ImGui::EndPopup(); +} + void section(const char* label, const std::vector& items, const char* item_icon, - const char* add_tooltip, const char* add_notice) { + const char* add_tooltip, const char* add_notice, SidebarItemKind kind) { const bool open = sidebar_section_header(label, static_cast(items.size()), add_tooltip, add_notice); if (open) { for (const auto& item : items) { ImGui::Selectable((std::string(" ") + item_icon + " " + item).c_str()); + sidebar_item_context(item, kind); } } ImGui::Separator(); @@ -211,8 +230,18 @@ void add_branch_node(BranchNode& root, const std::string& branch) { node->full_name = branch; } +void branch_context(const std::string& branch, bool remote) { + if (!ImGui::BeginPopupContextItem()) return; + if (!remote && ImGui::MenuItem(ICON_FA_CODE_BRANCH " Checkout")) + g_git_manager->checkoutBranch(repo(), branch, g_notice); + if (remote && ImGui::MenuItem(ICON_FA_DOWNLOAD " Fetch")) g_notice = "Remote fetch is not wired yet"; + ImGui::Separator(); + if (ImGui::MenuItem(ICON_FA_COPY " Copy branch name")) ImGui::SetClipboardText(branch.c_str()); + ImGui::EndPopup(); +} + void draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const char* root_group_icon, - const std::string& id_path = {}, int depth = 0) { + bool remote, const std::string& id_path = {}, int depth = 0) { for (const auto& [name, node] : parent.children) { const std::string id = id_path + "/" + name; if (!node.children.empty()) { @@ -220,18 +249,22 @@ void draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const const bool open = sidebar_collapse_row(id.c_str(), std::string(group_icon) + " " + name, true, 0); if (open) { ImGui::Indent(ui(17.0f)); - if (node.branch) ImGui::Selectable((std::string(branch_icon) + " " + name + "##branch" + id).c_str()); - draw_branch_nodes(node, branch_icon, root_group_icon, id, depth + 1); + if (node.branch) { + ImGui::Selectable((std::string(branch_icon) + " " + name + "##branch" + id).c_str()); + branch_context(node.full_name, remote); + } + draw_branch_nodes(node, branch_icon, root_group_icon, remote, id, depth + 1); ImGui::Unindent(ui(17.0f)); } } else { ImGui::Selectable((std::string(branch_icon) + " " + name + "##" + id).c_str()); + branch_context(node.full_name, remote); } } } void branch_section(const char* label, const std::vector& branches, const char* branch_icon, - const char* group_icon, const char* add_tooltip, const char* add_notice) { + const char* group_icon, const char* add_tooltip, const char* add_notice, bool remote) { const bool open = sidebar_section_header(label, static_cast(branches.size()), add_tooltip, add_notice); if (open) { BranchNode root; @@ -240,7 +273,7 @@ void branch_section(const char* label, const std::vector& branches, add_branch_node(root, branch); } ImGui::Indent(ui(17.0f)); - draw_branch_nodes(root, branch_icon, group_icon, label); + draw_branch_nodes(root, branch_icon, group_icon, remote, label); ImGui::Unindent(ui(17.0f)); } ImGui::Separator(); @@ -256,13 +289,13 @@ void draw_sidebar(float width) { ImGui::InputTextWithHint("##filter", ICON_FA_MAGNIFYING_GLASS " Search branches...", g_filter.data(), g_filter.size()); ImGui::Spacing(); branch_section(ICON_FA_HOUSE " LOCAL", repo().local_branches, ICON_FA_CODE_BRANCH, ICON_FA_FOLDER, - "Create local branch", "Branch creation is not wired yet"); + "Create local branch", "Branch creation is not wired yet", false); branch_section(ICON_FA_CLOUD " REMOTE", repo().remote_branches, ICON_FA_CODE_BRANCH, ICON_FA_GLOBE, - "Add remote", "Remote creation is not wired yet"); + "Add remote", "Remote creation is not wired yet", true); section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees, ICON_FA_COMPUTER, - "Add worktree", "Worktree creation is not wired yet"); + "Add worktree", "Worktree creation is not wired yet", SidebarItemKind::worktree); section(ICON_FA_CUBES " SUBMODULES", repo().submodules, ICON_FA_CUBES, - "Add submodule", "Submodule creation is not wired yet"); + "Add submodule", "Submodule creation is not wired yet", SidebarItemKind::submodule); ImGui::EndChild(); } @@ -295,6 +328,12 @@ void draw_ref_badge(const RefBadge& badge, int index, int lane) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(6.0f), ui(1.5f)}); const std::string id = text + "##ref_badge_" + std::to_string(index); ImGui::Button(id.c_str()); + if (ImGui::BeginPopupContextItem()) { + if (badge.kind == RefKind::local && ImGui::MenuItem(ICON_FA_CODE_BRANCH " Checkout")) + g_git_manager->checkoutBranch(repo(), badge.name, g_notice); + if (ImGui::MenuItem(ICON_FA_COPY " Copy ref name")) ImGui::SetClipboardText(badge.name.c_str()); + ImGui::EndPopup(); + } ImGui::PopStyleVar(2); ImGui::PopStyleColor(3); } @@ -366,6 +405,19 @@ void draw_commit_table() { std::string row_id = "##commit" + std::to_string(i); if (ImGui::Selectable(row_id.c_str(), repo().selected_commit == i, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) repo().selected_commit = i; + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem(ICON_FA_COPY " Copy commit hash")) { + char oid[GIT_OID_SHA1_HEXSIZE + 1]{}; + git_oid_tostr(oid, sizeof(oid), &commit.oid); + ImGui::SetClipboardText(oid); + } + if (ImGui::MenuItem(ICON_FA_COPY " Copy commit message")) + ImGui::SetClipboardText(commit.summary.c_str()); + ImGui::Separator(); + if (ImGui::MenuItem(ICON_FA_TAG " Create tag here")) g_notice = "Tag creation is not wired yet"; + if (ImGui::MenuItem(ICON_FA_CODE_BRANCH " Create branch here")) g_notice = "Branch creation is not wired yet"; + ImGui::EndPopup(); + } ImGui::SameLine(0, ui(3)); for (int ref_index = 0; ref_index < static_cast(commit.refs.size()); ++ref_index) { if (ref_index > 0) ImGui::SameLine(0, ui(4)); @@ -554,6 +606,15 @@ void draw_new_tab() { ImGuiSelectableFlags_AllowDoubleClick, {0, ui(42)})) { open_repository(recent[i].c_str()); } + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Open repository")) open_repository(recent[i].c_str()); + if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Reveal in file manager")) { + std::string error; + if (!izo::reveal_in_file_manager(recent[i], &error)) g_notice = error; + } + if (ImGui::MenuItem(ICON_FA_COPY " Copy path")) ImGui::SetClipboardText(recent[i].c_str()); + ImGui::EndPopup(); + } ImGui::SameLine(ui(260)); ImGui::TextDisabled("%s", recent[i].c_str()); ImGui::PopID(); @@ -598,6 +659,17 @@ void draw_app() { g_active_tab = i; ImGui::EndTabItem(); } + if (ImGui::BeginPopupContextItem()) { + if (!g_tabs[i]->path.empty() && ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Reveal in file manager")) { + std::string error; + if (!izo::reveal_in_file_manager(g_tabs[i]->path, &error)) g_notice = error; + } + if (!g_tabs[i]->path.empty() && ImGui::MenuItem(ICON_FA_COPY " Copy repository path")) + ImGui::SetClipboardText(g_tabs[i]->path.c_str()); + ImGui::Separator(); + if (ImGui::MenuItem(ICON_FA_XMARK " Close tab")) tab_to_close = i; + ImGui::EndPopup(); + } if (!open) tab_to_close = i; } ImGui::SameLine();