feat(context): add repository item menus and submodule updates

This commit is contained in:
2026-06-18 16:57:01 -05:00
parent 74b637c02c
commit 72de6aa7bc
3 changed files with 139 additions and 10 deletions

View File

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

View File

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

View File

@@ -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<std::string>& 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<int>(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<std::string>& 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<int>(branches.size()), add_tooltip, add_notice);
if (open) {
BranchNode root;
@@ -240,7 +273,7 @@ void branch_section(const char* label, const std::vector<std::string>& 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<int>(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();