From 2d451a5ece1bf0707c4e9db0130ade77bdb2725b Mon Sep 17 00:00:00 2001 From: GigabiteStudios Date: Thu, 18 Jun 2026 23:18:24 -0500 Subject: [PATCH] feat(toolbar): show dynamic undo and redo actions --- src/managers/git_manager.cpp | 175 ++++++++++++++++++++++++++++++++++- src/managers/git_manager.h | 1 + src/models/repository.h | 13 +++ src/ui/gitree_ui.cpp | 6 +- 4 files changed, 191 insertions(+), 4 deletions(-) diff --git a/src/managers/git_manager.cpp b/src/managers/git_manager.cpp index 6647bff..5a8c31d 100644 --- a/src/managers/git_manager.cpp +++ b/src/managers/git_manager.cpp @@ -16,6 +16,70 @@ 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 addBadge(RepositoryView &repository, const git_oid *oid, RefBadge badge) { if (!oid) @@ -406,6 +470,50 @@ void GitManager::loadWorkingTree(RepositoryView &repository) 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 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(); @@ -431,6 +539,7 @@ bool GitManager::loadRepositoryData(RepositoryView &repository, std::string &err repository.name = path.filename().string(); if (repository.name.empty()) repository.name = path.parent_path().filename().string(); + loadToolbarHistoryActions(repository); git_reference *head = nullptr; if (git_repository_head(&head, repository.repo) == 0) @@ -902,12 +1011,74 @@ bool GitManager::popStash(RepositoryView &repository, std::string &error) bool GitManager::undoCommit(RepositoryView &repository, std::string &error) { - return runGit(repository, {"reset", "--soft", "HEAD~1"}, "Last commit moved back to staging", 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 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) { - return runGit(repository, {"reset", "--soft", "HEAD@{1}"}, "Commit restored from reflog", 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 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) diff --git a/src/managers/git_manager.h b/src/managers/git_manager.h index 3632897..8bb4fcf 100644 --- a/src/managers/git_manager.h +++ b/src/managers/git_manager.h @@ -56,6 +56,7 @@ public: private: static std::string lastError(const char *fallback); static std::string formatTime(git_time_t value, int offset_minutes); + void loadToolbarHistoryActions(RepositoryView &repository); void readBranches(RepositoryView &repository, git_branch_t type, std::vector &output); void loadBranchDivergence(RepositoryView &repository); void loadRefBadges(RepositoryView &repository); diff --git a/src/models/repository.h b/src/models/repository.h index f8c8d4b..476e38d 100644 --- a/src/models/repository.h +++ b/src/models/repository.h @@ -34,6 +34,17 @@ struct BranchDivergence { size_t behind = 0; }; +enum class ToolbarHistoryActionKind { none, checkout, commit }; + +struct ToolbarHistoryAction { + ToolbarHistoryActionKind kind = ToolbarHistoryActionKind::none; + bool available = false; + std::string tooltip; + std::string source; + std::string target; + std::string summary; +}; + struct CommitInfo { git_oid oid{}; std::string short_id; @@ -74,6 +85,8 @@ struct RepositoryView { std::vector working_files; int selected_commit = 0; int scroll_to_commit = -1; + ToolbarHistoryAction undo_action; + ToolbarHistoryAction redo_action; RepositoryView() = default; ~RepositoryView() { close(); } diff --git a/src/ui/gitree_ui.cpp b/src/ui/gitree_ui.cpp index 95731d6..ff4a545 100644 --- a/src/ui/gitree_ui.cpp +++ b/src/ui/gitree_ui.cpp @@ -3141,10 +3141,12 @@ void draw_app() { const bool toolbar_busy = g_running_toolbar_action != ToolbarActionRequest::none; ImGui::SetCursorPos({std::max(selectors_right + ui(10.0f), centered_x), ui(2.0f)}); - if (toolbar_action("undo", "Undo", ICON_TB_ROTATE_LEFT, "Undo last Git action", true, false, 54)) + if (toolbar_action("undo", "Undo", ICON_TB_ROTATE_LEFT, repo().undo_action.tooltip.c_str(), + repo().undo_action.available, false, 54)) g_git_manager->undoCommit(repo(), g_notice); ImGui::SameLine(0, action_spacing); - if (toolbar_action("redo", "Redo", ICON_TB_ROTATE_RIGHT, "Redo last Git action", true, false, 54)) + if (toolbar_action("redo", "Redo", ICON_TB_ROTATE_RIGHT, repo().redo_action.tooltip.c_str(), + repo().redo_action.available, false, 54)) g_git_manager->redoCommit(repo(), g_notice); ImGui::SameLine(0, action_spacing); bool pull_dropdown_clicked = false;