feat(toolbar): show dynamic undo and redo actions

This commit is contained in:
2026-06-18 23:18:24 -05:00
parent a64770b684
commit 2d451a5ece
4 changed files with 191 additions and 4 deletions

View File

@@ -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<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();
@@ -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<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)
{
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<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)

View File

@@ -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<std::string> &output);
void loadBranchDivergence(RepositoryView &repository);
void loadRefBadges(RepositoryView &repository);

View File

@@ -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<WorkingFile> working_files;
int selected_commit = 0;
int scroll_to_commit = -1;
ToolbarHistoryAction undo_action;
ToolbarHistoryAction redo_action;
RepositoryView() = default;
~RepositoryView() { close(); }

View File

@@ -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;