From 2c9b370ed9377010daf88396e110f627705a007f Mon Sep 17 00:00:00 2001 From: GigabiteStudios Date: Thu, 18 Jun 2026 19:34:51 -0500 Subject: [PATCH] feat(ui): wire Git actions and operation dialogs --- src/ui/gitree_ui.cpp | 348 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 302 insertions(+), 46 deletions(-) diff --git a/src/ui/gitree_ui.cpp b/src/ui/gitree_ui.cpp index bc117af..7a2592f 100644 --- a/src/ui/gitree_ui.cpp +++ b/src/ui/gitree_ui.cpp @@ -40,9 +40,21 @@ std::string g_notice; bool g_init_popup = false; bool g_about_popup = false; bool g_licenses_popup = false; +bool g_branch_create_popup = false; +bool g_tag_create_popup = false; +bool g_remote_add_popup = false; +bool g_worktree_add_popup = false; +bool g_submodule_add_popup = false; +bool g_discard_all_popup = false; +std::array g_git_name{}; +std::array g_git_value{}; +std::array g_git_path{}; +std::string g_git_target; enum class FileViewMode { path, tree }; FileViewMode g_file_view_mode = FileViewMode::path; bool g_view_all_files = false; +bool g_sidebar_workspace_view = false; +bool g_request_branch_selector = false; std::array g_commit_summary{}; std::array g_commit_description{}; float g_ui_scale = 1.0f; @@ -289,7 +301,17 @@ bool sidebar_section_header(const char* label, int count, const char* add_toolti ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.25f, 0.72f, 0.35f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(5.0f), ui(1.0f)}); - if (ImGui::SmallButton(ICON_FA_PLUS "##add")) g_notice = add_notice; + if (ImGui::SmallButton(ICON_FA_PLUS "##add")) { + const std::string section = label; + if (section.find("LOCAL") != std::string::npos) { + g_git_target.clear(); + g_branch_create_popup = true; + } + else if (section.find("REMOTE") != std::string::npos) g_remote_add_popup = true; + else if (section.find("WORKTREES") != std::string::npos) g_worktree_add_popup = true; + else if (section.find("SUBMODULES") != std::string::npos) g_submodule_add_popup = true; + else g_notice = add_notice; + } if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("%s", add_tooltip); ImGui::PopStyleVar(); ImGui::PopStyleColor(2); @@ -344,7 +366,8 @@ void section(const char* label, const std::vector& items, const cha const bool open = sidebar_section_header(label, static_cast(items.size()), add_tooltip, add_notice); if (open && body_height >= ui(1.0f)) { ImGui::BeginChild((std::string(label) + "##body").c_str(), {-1, body_height}); - for (const auto& item : items) { + const std::vector snapshot = items; + for (const auto& item : snapshot) { sidebar_item_row(item_icon, item, item); sidebar_item_context(item, kind); } @@ -379,7 +402,10 @@ 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"; + if (remote && ImGui::MenuItem(ICON_FA_DOWNLOAD " Fetch")) { + const size_t slash = branch.find('/'); + g_git_manager->fetch(repo(), slash == std::string::npos ? branch : branch.substr(0, slash), g_notice); + } ImGui::Separator(); if (ImGui::MenuItem(ICON_FA_COPY " Copy branch name")) ImGui::SetClipboardText(branch.c_str()); ImGui::EndPopup(); @@ -434,12 +460,26 @@ void draw_sidebar(float width) { ImGui::BeginChild("sidebar", {width, -ui(28.0f)}, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); const float mode_button_width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(ICON_FA_LIST " List", {mode_button_width, ui(30)})) {} + const bool list_selected = !g_sidebar_workspace_view; + if (list_selected) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.25f, 0.40f, 1.0f)); + if (ImGui::Button(ICON_FA_LIST " List", {mode_button_width, ui(30)})) g_sidebar_workspace_view = false; + if (list_selected) ImGui::PopStyleColor(); ImGui::SameLine(); - if (ImGui::Button(ICON_FA_LAYER_GROUP " Workspace", {mode_button_width, ui(30)})) {} - ImGui::TextDisabled(ICON_FA_EYE " VIEWING %d", static_cast(repo().local_branches.size() + repo().remote_branches.size())); + const bool workspace_selected = g_sidebar_workspace_view; + if (workspace_selected) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.25f, 0.40f, 1.0f)); + if (ImGui::Button(ICON_FA_LAYER_GROUP " Workspace", {mode_button_width, ui(30)})) + g_sidebar_workspace_view = true; + if (workspace_selected) ImGui::PopStyleColor(); + const int viewing_count = g_sidebar_workspace_view + ? static_cast(repo().worktrees.size() + repo().submodules.size()) + : static_cast(repo().local_branches.size() + repo().remote_branches.size()); + ImGui::TextDisabled(ICON_FA_EYE " VIEWING %d", viewing_count); ImGui::SetNextItemWidth(-1); - ImGui::InputTextWithHint("##filter", ICON_FA_MAGNIFYING_GLASS " Search branches...", g_filter.data(), g_filter.size()); + ImGui::InputTextWithHint("##filter", g_sidebar_workspace_view + ? ICON_FA_MAGNIFYING_GLASS " Search workspace..." + : ICON_FA_MAGNIFYING_GLASS " Search branches...", g_filter.data(), g_filter.size()); ImGui::Spacing(); ImGui::BeginChild("sidebar_sections", {-1, -1}, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); @@ -455,11 +495,15 @@ void draw_sidebar(float width) { std::array section_open{}; int open_count = 0; for (size_t index = 0; index < section_open.size(); ++index) { + if (g_sidebar_workspace_view ? index < 2 : index >= 2) { + section_open[index] = false; + continue; + } section_open[index] = sidebar_section_is_open(section_ids[index]); if (!section_open[index]) continue; ++open_count; } - const float header_space = (ui(22.0f) + 1.0f) * static_cast(section_open.size()); + const float header_space = (ui(22.0f) + 1.0f) * 2.0f; const float splitter_space = ui(5.0f) * open_count; const float body_space = std::max(0.0f, ImGui::GetContentRegionAvail().y - header_space - splitter_space); std::array section_heights{}; @@ -488,13 +532,16 @@ void draw_sidebar(float width) { maximum_heights[index] = std::max(1.0f, logical_body_space - other_heights); } - 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", false, 0, section_heights[0], maximum_heights[0]); - branch_section(ICON_FA_CLOUD " REMOTE", repo().remote_branches, ICON_FA_CODE_BRANCH, ICON_FA_GLOBE, - "Add remote", "Remote creation is not wired yet", true, 1, section_heights[1], maximum_heights[1]); - section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees, ICON_FA_COMPUTER, - "Add worktree", "Worktree creation is not wired yet", SidebarItemKind::worktree, 2, - section_heights[2], maximum_heights[2]); + if (!g_sidebar_workspace_view) { + branch_section(ICON_FA_HOUSE " LOCAL", repo().local_branches, ICON_FA_CODE_BRANCH, ICON_FA_FOLDER, + "Create local branch", "Create local branch", false, 0, section_heights[0], maximum_heights[0]); + branch_section(ICON_FA_CLOUD " REMOTE", repo().remote_branches, ICON_FA_CODE_BRANCH, ICON_FA_GLOBE, + "Add remote", "Add remote", true, 1, section_heights[1], maximum_heights[1]); + } else { + section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees, ICON_FA_COMPUTER, + "Add worktree", "Add worktree", SidebarItemKind::worktree, 2, + section_heights[2], maximum_heights[2]); + } const float submodules_body_height = section_open[3] && section_heights[3] >= ui(1.0f) ? section_heights[3] + ui(5.0f) @@ -502,9 +549,11 @@ void draw_sidebar(float width) { const float submodules_block_height = ui(22.0f) + submodules_body_height; ImGui::SetCursorPosY(std::max(ImGui::GetCursorPosY(), ImGui::GetContentRegionMax().y - submodules_block_height)); - section(ICON_FA_CUBES " SUBMODULES", repo().submodules, ICON_FA_CUBES, - "Add submodule", "Submodule creation is not wired yet", SidebarItemKind::submodule, 3, - section_heights[3], maximum_heights[3]); + if (g_sidebar_workspace_view) { + section(ICON_FA_CUBES " SUBMODULES", repo().submodules, ICON_FA_CUBES, + "Add submodule", "Add submodule", SidebarItemKind::submodule, 3, + section_heights[3], maximum_heights[3]); + } ImGui::PopStyleVar(); ImGui::EndChild(); ImGui::EndChild(); @@ -680,8 +729,18 @@ void draw_commit_table() { 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"; + if (ImGui::MenuItem(ICON_FA_TAG " Create tag here")) { + char target[GIT_OID_SHA1_HEXSIZE + 1]{}; + git_oid_tostr(target, sizeof(target), &commit.oid); + g_git_target = target; + g_tag_create_popup = true; + } + if (ImGui::MenuItem(ICON_FA_CODE_BRANCH " Create branch here")) { + char target[GIT_OID_SHA1_HEXSIZE + 1]{}; + git_oid_tostr(target, sizeof(target), &commit.oid); + g_git_target = target; + g_branch_create_popup = true; + } ImGui::EndPopup(); } float chip_x = reference_origin.x + ui(3.0f); @@ -722,7 +781,8 @@ ImVec4 change_color(FileChangeKind kind) { return {0.94f, 0.66f, 0.25f, 1}; } -void draw_file_row(const std::string& path, FileChangeKind kind, int id) { +void draw_file_row(const std::string& path, FileChangeKind kind, int id, + bool working_file = false, bool staged = false, const std::string& action_path = {}) { ImGui::PushID(id); ImGui::InvisibleButton("##file", {-1, ui(25.0f)}); const ImVec2 minimum = ImGui::GetItemRectMin(); @@ -732,7 +792,15 @@ void draw_file_row(const std::string& path, FileChangeKind kind, int id) { const float y = minimum.y + (maximum.y - minimum.y - ImGui::GetFontSize()) * 0.5f; draw->AddText({minimum.x + ui(4.0f), y}, ImGui::ColorConvertFloat4ToU32(change_color(kind)), change_icon(kind)); draw->AddText({minimum.x + ui(20.0f), y}, IM_COL32(174, 179, 187, 255), path.c_str()); + const std::string& git_path = action_path.empty() ? path : action_path; if (ImGui::BeginPopupContextItem()) { + if (working_file && !staged && ImGui::MenuItem(ICON_FA_PLUS " Stage file")) + g_git_manager->stageFile(repo(), git_path, g_notice); + if (working_file && staged && ImGui::MenuItem(ICON_FA_MINUS " Unstage file")) + g_git_manager->unstageFile(repo(), git_path, g_notice); + if (working_file && !staged && ImGui::MenuItem(ICON_FA_TRASH_CAN " Discard changes")) + g_git_manager->discardFile(repo(), git_path, g_notice); + if (working_file) ImGui::Separator(); if (ImGui::MenuItem(ICON_FA_COPY " Copy path")) ImGui::SetClipboardText(path.c_str()); ImGui::EndPopup(); } @@ -760,16 +828,19 @@ void draw_file_toolbar(bool show_view_all) { template void draw_files(const std::vector& files, bool staged_filter = false, bool filter_by_stage = false) { + // Git actions reload repository data immediately, so render from a stable snapshot. + const std::vector snapshot = files; if (g_file_view_mode == FileViewMode::path) { int id = 0; - for (const auto& file : files) { + for (const auto& file : snapshot) { if constexpr (requires { file.staged; }) if (filter_by_stage && file.staged != staged_filter) continue; - draw_file_row(file.path, file.kind, id++); + if constexpr (requires { file.staged; }) draw_file_row(file.path, file.kind, id++, true, file.staged); + else draw_file_row(file.path, file.kind, id++); } return; } std::map> groups; - for (const auto& file : files) { + for (const auto& file : snapshot) { if constexpr (requires { file.staged; }) if (filter_by_stage && file.staged != staged_filter) continue; const size_t slash = file.path.find('/'); const std::string group = slash == std::string::npos ? "Files" : file.path.substr(0, slash); @@ -780,7 +851,10 @@ void draw_files(const std::vector& files, bool staged_filter = false, bool if (ImGui::TreeNodeEx((std::string(ICON_FA_FOLDER) + " " + group).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { for (const File* file : entries) { const size_t slash = file->path.find('/'); - draw_file_row(slash == std::string::npos ? file->path : file->path.substr(slash + 1), file->kind, id++); + const std::string display = slash == std::string::npos ? file->path : file->path.substr(slash + 1); + if constexpr (requires { file->staged; }) + draw_file_row(display, file->kind, id++, true, file->staged, file->path); + else draw_file_row(display, file->kind, id++); } ImGui::TreePop(); } @@ -799,7 +873,7 @@ void draw_working_details() { ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.38f, 0.13f, 0.14f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.88f, 0.36f, 0.36f, 1.0f)); if (ImGui::Button(ICON_FA_TRASH_CAN "##discard_all", {ui(23.0f), ui(23.0f)})) - g_notice = "Discard all is not wired yet"; + g_discard_all_popup = true; ImGui::PopStyleColor(3); const std::string changes_label = std::to_string(repo().working_files.size()) + " file changes on"; @@ -813,15 +887,18 @@ void draw_working_details() { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.06f, 0.37f, 0.43f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.08f, 0.44f, 0.51f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(5.0f), ui(1.0f)}); - ImGui::Button((repo().branch + "##working_branch").c_str()); + if (ImGui::Button((repo().branch + "##working_branch").c_str())) + g_request_branch_selector = true; ImGui::PopStyleVar(); ImGui::PopStyleColor(2); ImGui::SameLine(ImGui::GetWindowWidth() - ui(27.0f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.19f, 0.08f, 0.38f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.48f, 0.14f, 0.86f, 1.0f)); - if (ImGui::Button(ICON_FA_WAND_MAGIC_SPARKLES "##working_assist", {ui(23.0f), ui(23.0f)})) - g_notice = "Commit assistance is not wired yet"; + if (ImGui::Button(ICON_FA_WAND_MAGIC_SPARKLES "##working_assist", {ui(23.0f), ui(23.0f)})) { + const std::string suggestion = "Update " + std::to_string(repo().working_files.size()) + " files"; + std::snprintf(g_commit_summary.data(), g_commit_summary.size(), "%s", suggestion.c_str()); + } ImGui::PopStyleColor(2); ImGui::EndChild(); ImGui::Separator(); @@ -844,7 +921,7 @@ void draw_working_details() { ImGui::SameLine(std::max(ui(145.0f), ImGui::GetWindowWidth() - ui(119.0f))); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.27f, 0.18f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.24f, 0.72f, 0.35f, 1.0f)); - if (ImGui::SmallButton("Stage All Changes")) g_notice = "Staging is not wired yet"; + if (ImGui::SmallButton("Stage All Changes")) g_git_manager->stageAll(repo(), g_notice); ImGui::PopStyleColor(2); ImGui::Separator(); if (unstaged_open) { @@ -894,8 +971,10 @@ void draw_working_details() { ImGui::SameLine(0, ui(4.0f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.12f, 0.29f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.35f, 0.16f, 0.62f, 1.0f)); - if (ImGui::SmallButton(ICON_FA_WAND_MAGIC_SPARKLES "##summary_assist")) - g_notice = "Summary assistance is not wired yet"; + if (ImGui::SmallButton(ICON_FA_WAND_MAGIC_SPARKLES "##summary_assist")) { + const std::string suggestion = "Update " + std::to_string(repo().working_files.size()) + " files"; + std::snprintf(g_commit_summary.data(), g_commit_summary.size(), "%s", suggestion.c_str()); + } ImGui::PopStyleColor(2); ImGui::SetNextItemWidth(-1); ImGui::InputTextMultiline("##commit_description", g_commit_description.data(), g_commit_description.size(), @@ -913,15 +992,25 @@ void draw_working_details() { ImGui::SameLine(std::max(ui(130.0f), ImGui::GetWindowWidth() - ui(181.0f))); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20f, 0.06f, 0.40f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.48f, 0.12f, 0.82f, 1.0f)); - if (ImGui::SmallButton(ICON_FA_WAND_MAGIC_SPARKLES " Compose commits with AI")) - g_notice = "Commit composition is not wired yet"; + if (ImGui::SmallButton(ICON_FA_WAND_MAGIC_SPARKLES " Compose commits with AI")) { + const std::string suggestion = "Update " + std::to_string(repo().working_files.size()) + " files"; + std::snprintf(g_commit_summary.data(), g_commit_summary.size(), "%s", suggestion.c_str()); + if (g_commit_description[0] == '\0') + std::snprintf(g_commit_description.data(), g_commit_description.size(), + "Update the current working tree changes."); + } ImGui::PopStyleColor(2); ImGui::BeginDisabled(staged == 0 || g_commit_summary[0] == '\0'); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.24f, 0.18f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.24f, 0.54f, 0.32f, 1.0f)); - if (ImGui::Button(ICON_FA_CIRCLE_DOT " Stage Changes to Commit", {-1, ui(40.0f)})) - g_notice = "Commit creation is not wired yet"; + if (ImGui::Button(ICON_FA_CIRCLE_DOT " Stage Changes to Commit", {-1, ui(40.0f)})) { + if (g_git_manager->commit(repo(), g_commit_summary.data(), g_commit_description.data(), amend, g_notice)) { + g_commit_summary.fill('\0'); + g_commit_description.fill('\0'); + amend = false; + } + } ImGui::PopStyleColor(2); ImGui::EndDisabled(); ImGui::EndChild(); @@ -1179,6 +1268,150 @@ void draw_licenses_popup() { ImGui::EndPopup(); } +void draw_git_action_popups() { + if (g_branch_create_popup) { + g_git_name.fill('\0'); + g_branch_create_popup = false; + ImGui::OpenPopup("Create branch"); + } + if (ImGui::BeginPopupModal("Create branch", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + static bool checkout_branch = true; + ImGui::TextUnformatted("Branch name"); + ImGui::SetNextItemWidth(ui(360.0f)); + ImGui::InputText("##branch_name", g_git_name.data(), g_git_name.size()); + ImGui::TextDisabled("Start point: %s", g_git_target.empty() ? "HEAD" : g_git_target.c_str()); + ImGui::Checkbox("Check out new branch", &checkout_branch); + const bool valid = g_git_name[0] != '\0'; + ImGui::BeginDisabled(!valid); + if (ImGui::Button("Create", {ui(90.0f), 0}) && + g_git_manager->createBranch(repo(), g_git_name.data(), g_git_target, + checkout_branch, g_notice)) { + g_git_target.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", {ui(90.0f), 0})) { + g_git_target.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (g_tag_create_popup) { + g_git_name.fill('\0'); + g_tag_create_popup = false; + ImGui::OpenPopup("Create tag"); + } + if (ImGui::BeginPopupModal("Create tag", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted("Tag name"); + ImGui::SetNextItemWidth(ui(360.0f)); + ImGui::InputText("##tag_name", g_git_name.data(), g_git_name.size()); + ImGui::TextDisabled("Target: %s", g_git_target.empty() ? "HEAD" : g_git_target.c_str()); + ImGui::BeginDisabled(g_git_name[0] == '\0'); + if (ImGui::Button("Create", {ui(90.0f), 0}) && + g_git_manager->createTag(repo(), g_git_name.data(), g_git_target, g_notice)) { + g_git_target.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", {ui(90.0f), 0})) { + g_git_target.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (g_remote_add_popup) { + g_git_name.fill('\0'); + g_git_value.fill('\0'); + std::snprintf(g_git_name.data(), g_git_name.size(), "%s", + std::find(repo().remotes.begin(), repo().remotes.end(), "origin") == repo().remotes.end() + ? "origin" : "upstream"); + g_remote_add_popup = false; + ImGui::OpenPopup("Add remote"); + } + if (ImGui::BeginPopupModal("Add remote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted("Remote name"); + ImGui::SetNextItemWidth(ui(420.0f)); + ImGui::InputText("##remote_name", g_git_name.data(), g_git_name.size()); + ImGui::TextUnformatted("Remote URL"); + ImGui::SetNextItemWidth(ui(420.0f)); + ImGui::InputText("##remote_url", g_git_value.data(), g_git_value.size()); + ImGui::BeginDisabled(g_git_name[0] == '\0' || g_git_value[0] == '\0'); + if (ImGui::Button("Add remote", {ui(110.0f), 0}) && + g_git_manager->addRemote(repo(), g_git_name.data(), g_git_value.data(), g_notice)) + ImGui::CloseCurrentPopup(); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + if (g_worktree_add_popup) { + g_git_path.fill('\0'); + g_git_value.fill('\0'); + g_worktree_add_popup = false; + ImGui::OpenPopup("Add worktree"); + } + if (ImGui::BeginPopupModal("Add worktree", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted("Worktree folder"); + ImGui::SetNextItemWidth(ui(460.0f)); + ImGui::InputText("##worktree_path", g_git_path.data(), g_git_path.size()); + ImGui::TextUnformatted("Branch or commit (optional)"); + ImGui::SetNextItemWidth(ui(460.0f)); + ImGui::InputText("##worktree_branch", g_git_value.data(), g_git_value.size()); + ImGui::BeginDisabled(g_git_path[0] == '\0'); + if (ImGui::Button("Add worktree", {ui(120.0f), 0}) && + g_git_manager->addWorktree(repo(), g_git_path.data(), g_git_value.data(), g_notice)) + ImGui::CloseCurrentPopup(); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + if (g_submodule_add_popup) { + g_git_value.fill('\0'); + g_git_path.fill('\0'); + g_submodule_add_popup = false; + ImGui::OpenPopup("Add submodule"); + } + if (ImGui::BeginPopupModal("Add submodule", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted("Repository URL"); + ImGui::SetNextItemWidth(ui(460.0f)); + ImGui::InputText("##submodule_url", g_git_value.data(), g_git_value.size()); + ImGui::TextUnformatted("Local path (optional)"); + ImGui::SetNextItemWidth(ui(460.0f)); + ImGui::InputText("##submodule_path", g_git_path.data(), g_git_path.size()); + ImGui::BeginDisabled(g_git_value[0] == '\0'); + if (ImGui::Button("Add submodule", {ui(125.0f), 0}) && + g_git_manager->addSubmodule(repo(), g_git_value.data(), g_git_path.data(), g_notice)) + ImGui::CloseCurrentPopup(); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + if (g_discard_all_popup) { + g_discard_all_popup = false; + ImGui::OpenPopup("Discard all changes?"); + } + if (ImGui::BeginPopupModal("Discard all changes?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextWrapped("This permanently discards tracked changes and removes untracked files and folders."); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.42f, 0.12f, 0.13f, 1.0f)); + if (ImGui::Button("Discard all", {ui(110.0f), 0})) { + if (g_git_manager->discardAll(repo(), g_notice)) ImGui::CloseCurrentPopup(); + } + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } +} + void draw_popups() { if (g_init_popup) { ImGui::OpenPopup("Create repository"); g_init_popup = false; } if (ImGui::BeginPopupModal("Create repository", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { @@ -1195,6 +1428,7 @@ void draw_popups() { } draw_about_popup(); draw_licenses_popup(); + draw_git_action_popups(); } void draw_footer() { @@ -1272,7 +1506,10 @@ void draw_app() { ImGui::EndMenu(); } if (ImGui::BeginMenu("Edit")) { ImGui::MenuItem("Preferences", nullptr, false, false); ImGui::EndMenu(); } - if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Refresh", "F5"); ImGui::EndMenu(); } + if (ImGui::BeginMenu("View")) { + if (ImGui::MenuItem("Refresh", "F5")) g_git_manager->reload(repo(), g_notice); + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("About Gitree")) g_about_popup = true; ImGui::EndMenu(); @@ -1377,12 +1614,26 @@ void draw_app() { const float repository_width = std::clamp( ImGui::CalcTextSize(repo().name.c_str()).x / g_ui_scale + 42.0f, 120.0f, 220.0f); if (toolbar_selector("repository", "repository", repo().name, repository_width)) - g_notice = "Use repository tabs to switch repositories"; + ImGui::OpenPopup("repository_selector"); + if (ImGui::BeginPopup("repository_selector")) { + for (size_t index = 0; index < g_tabs.size(); ++index) { + if (ImGui::MenuItem(g_tabs[index]->name.c_str(), nullptr, index == g_active_tab)) { + g_active_tab = index; + g_tab_to_select = g_tabs[index].get(); + persist_repository_session(); + } + } + ImGui::EndPopup(); + } ImGui::SameLine(0, ui(3)); ImGui::SetCursorPosY(ui(19)); ImGui::TextDisabled(ICON_FA_ANGLE_RIGHT); ImGui::SameLine(0, ui(3)); ImGui::SetCursorPosY(ui(1)); + if (g_request_branch_selector) { + ImGui::OpenPopup("branch_selector"); + g_request_branch_selector = false; + } if (toolbar_selector("branch", "branch", repo().branch, 150.0f)) ImGui::OpenPopup("branch_selector"); const float selectors_right = ImGui::GetItemRectMax().x - ImGui::GetWindowPos().x; ImGui::SetNextWindowSize({ui(320.0f), ui(370.0f)}, ImGuiCond_Appearing); @@ -1413,26 +1664,31 @@ void draw_app() { ImGui::SetCursorPos({std::max(selectors_right + ui(10.0f), centered_x), ui(1.0f)}); if (toolbar_action("undo", "Undo", ICON_FA_ROTATE_LEFT, "Undo last Git action", true, false, 54)) - g_notice = "Undo is not wired yet"; + g_git_manager->undoCommit(repo(), g_notice); ImGui::SameLine(0, action_spacing); - toolbar_action("redo", "Redo", ICON_FA_ROTATE_RIGHT, "Redo last Git action", false, false, 54); + if (toolbar_action("redo", "Redo", ICON_FA_ROTATE_RIGHT, "Redo last Git action", true, false, 54)) + g_git_manager->redoCommit(repo(), g_notice); ImGui::SameLine(0, action_spacing); bool pull_dropdown_clicked = false; if (toolbar_action("pull", "Pull", ICON_FA_DOWNLOAD, pull_mode_name(g_user_data->pullMode()), true, true, 58, &pull_dropdown_clicked)) - g_notice = std::string(pull_mode_name(g_user_data->pullMode())) + " is not wired yet"; + g_git_manager->pull(repo(), g_user_data->pullMode(), g_notice); if (pull_dropdown_clicked) ImGui::OpenPopup("pull_options"); draw_pull_options(); ImGui::SameLine(0, action_spacing); if (toolbar_action("push", "Push", ICON_FA_UPLOAD, "Push to remote", true, false, 58)) - g_notice = "Push is not wired yet"; + g_git_manager->push(repo(), g_notice); ImGui::SameLine(0, action_spacing); - if (toolbar_action("branch_action", "Branch", ICON_FA_CODE_BRANCH, "Create branch", true, false, 64)) - g_notice = "Branch creation is not wired yet"; + if (toolbar_action("branch_action", "Branch", ICON_FA_CODE_BRANCH, "Create branch", true, false, 64)) { + g_git_target.clear(); + g_branch_create_popup = true; + } ImGui::SameLine(0, action_spacing); - toolbar_action("stash", "Stash", ICON_FA_BOX_ARCHIVE, "Stash changes", false, false, 58); + if (toolbar_action("stash", "Stash", ICON_FA_BOX_ARCHIVE, "Stash changes", true, false, 58)) + g_git_manager->stash(repo(), g_notice); ImGui::SameLine(0, action_spacing); - toolbar_action("pop", "Pop", ICON_FA_BOX_OPEN, "Pop latest stash", false, false, 54); + if (toolbar_action("pop", "Pop", ICON_FA_BOX_OPEN, "Pop latest stash", true, false, 54)) + g_git_manager->popStash(repo(), g_notice); ImGui::EndChild(); ImGui::PopStyleColor();