Merge pull request 'main' (#2) from main into prod
All checks were successful
Build Windows EXE / build-windows (push) Successful in 23m29s

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-06-19 02:18:50 +00:00
8 changed files with 541 additions and 180 deletions

View File

@@ -70,6 +70,8 @@ add_executable(gitree WIN32
src/managers/user_data.h
src/managers/avatar_cache.cpp
src/managers/avatar_cache.h
src/managers/application_manager.cpp
src/managers/application_manager.h
src/models/repository.h
)
target_include_directories(gitree PRIVATE src vendor/libgit2/include vendor/icons)

103
README.md
View File

@@ -1,42 +1,93 @@
# Gitree
A native Dear ImGui Git client built with libgit2 and GLFW.
[![Windows build](https://dock-it.dev/Idea-Studios/Gitree/actions/workflows/windows-build.yml/badge.svg?branch=prod)](https://dock-it.dev/Idea-Studios/Gitree/actions?workflow=windows-build.yml)
[![Latest release](https://img.shields.io/gitea/v/release/Idea-Studios/Gitree?gitea_url=https%3A%2F%2Fdock-it.dev&label=release)](https://dock-it.dev/Idea-Studios/Gitree/releases/latest)
[![Platform](https://img.shields.io/badge/platform-Windows_x64-0078D4?logo=windows)](https://dock-it.dev/Idea-Studios/Gitree/releases)
[![C++](https://img.shields.io/badge/C%2B%2B-20-00599C?logo=cplusplus)](https://en.cppreference.com/w/cpp/20)
[![License](https://img.shields.io/badge/license-CC_BY--SA_4.0-lightgrey.svg)](https://dock-it.dev/Idea-Studios/Gitree/src/branch/prod/LICENSE)
## Build on Windows
A fast, native Git desktop client for Windows.
Clone with submodules, then run `run.bat`. The script configures a Release build,
builds Gitree and launches it. You can pass a repository path to open it directly:
## Download
Download the current Windows x64 executable from the
[latest release](https://dock-it.dev/Idea-Studios/Gitree/releases/latest), or browse
[all releases](https://dock-it.dev/Idea-Studios/Gitree/releases).
No installer is required: download the executable and run it.
## Use Gitree
Open Gitree, choose **Open repository**, and select a local Git repository. You can
also pass its path when launching from a terminal:
```powershell
.\Gitree-windows-x64.exe C:\path\to\repository
```
Use the sidebar to switch branches and browse refs. Select a commit to inspect its
details and changes. The commit area lets you stage files and create commits.
## Build from source
### Requirements
- Windows 10 or later (x64)
- [Git](https://git-scm.com/download/win)
- [CMake 3.21 or later](https://cmake.org/download/)
- A C++20 compiler: Visual Studio 2022 with the **Desktop development with C++**
workload, or a recent MinGW-w64 toolchain
- Ninja is optional; `run.bat` uses it automatically when available
Clone the repository with all submodules:
```powershell
git clone --recurse-submodules https://dock-it.dev/Idea-Studios/Gitree.git
cd Gitree
```
If the repository was cloned without `--recurse-submodules`, initialize the
dependencies before configuring the build:
```powershell
git submodule update --init --recursive
```
### Build and run automatically
From a Developer PowerShell or Command Prompt:
```bat
run.bat
```
`run.bat` configures a Release build, builds Gitree, and launches it. Pass a
repository path to open it immediately:
```bat
run.bat C:\path\to\repository
```
Manual build:
### Build with Visual Studio 2022
```bat
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
```powershell
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release --parallel
.\build\bin\Release\gitree.exe
```
Vendored dependencies are pinned under `vendor/` and compiled as static libraries:
### Build with Ninja
- libgit2 1.9.4
- Dear ImGui 1.92.8 (docking branch)
- GLFW 3.4
- iZo 0.1.0
Run these commands in a shell where your compiler is available. For MSVC, use a
Visual Studio Developer PowerShell or Developer Command Prompt.
The bundled Inter variable font is licensed under the SIL Open Font License.
Font Awesome Free icons are bundled from Attascii and merged into the ImGui font atlas.
```powershell
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
.\build\bin\gitree.exe
```
User settings, ImGui layout state, and recently closed repository history are stored in
`%APPDATA%\Identity\Gitree` on Windows. The directory is created automatically.
Commit avatars are resolved from normalized author emails through Gravatar and cached in
the `avatars` subdirectory; an identicon is used when an account has no custom image.
## License
## Source layout
- `src/managers/` owns Git, window, user-data, and avatar services.
- `src/models/` contains repository and commit data models.
- `src/ui/` contains the Dear ImGui application interface.
- `src/main.cpp` is the minimal process entry point.
Gitree is licensed under the [Creative Commons Attribution-ShareAlike 4.0
International license](https://dock-it.dev/Idea-Studios/Gitree/src/branch/prod/LICENSE).

View File

@@ -0,0 +1,122 @@
#include "managers/application_manager.h"
#include <izo/Process.hpp>
#include <algorithm>
#include <cstdlib>
namespace {
std::filesystem::path environmentPath(const char* name) {
const char* value = std::getenv(name);
return value && *value ? std::filesystem::path(value) : std::filesystem::path{};
}
std::filesystem::path firstExisting(std::initializer_list<std::filesystem::path> candidates) {
std::error_code error;
for (const auto& candidate : candidates)
if (!candidate.empty() && std::filesystem::is_regular_file(candidate, error)) return candidate;
return {};
}
std::filesystem::path findExecutable(const std::filesystem::path& root, const char* filename) {
std::error_code error;
if (root.empty() || !std::filesystem::is_directory(root, error)) return {};
for (std::filesystem::recursive_directory_iterator iterator(
root, std::filesystem::directory_options::skip_permission_denied, error), end;
iterator != end && !error; iterator.increment(error)) {
if (iterator->is_regular_file(error) && iterator->path().filename() == filename)
return iterator->path();
}
return {};
}
}
ApplicationManager::ApplicationManager() {
const auto local = environmentPath("LOCALAPPDATA");
const auto program_files = environmentPath("ProgramFiles");
const auto program_files_x86 = environmentPath("ProgramFiles(x86)");
const auto windows = environmentPath("WINDIR");
auto add = [this](ExternalApplicationId id, const char* name, std::filesystem::path executable,
std::vector<std::string> arguments = {}) {
const bool available = !executable.empty();
targets_.push_back({{id, name, available}, std::move(executable), std::move(arguments)});
applications_.push_back(targets_.back().info);
};
add(ExternalApplicationId::visual_studio_code, "VS Code", firstExisting({
local / "Programs/Microsoft VS Code/Code.exe",
program_files / "Microsoft VS Code/Code.exe",
program_files_x86 / "Microsoft VS Code/Code.exe",
}));
add(ExternalApplicationId::visual_studio, "Visual Studio", firstExisting({
program_files / "Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe",
program_files / "Microsoft Visual Studio/2022/Professional/Common7/IDE/devenv.exe",
program_files / "Microsoft Visual Studio/2022/Enterprise/Common7/IDE/devenv.exe",
program_files_x86 / "Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe",
}));
add(ExternalApplicationId::antigravity, "Antigravity", firstExisting({
local / "Programs/Antigravity/Antigravity.exe",
local / "Antigravity/Antigravity.exe",
program_files / "Antigravity/Antigravity.exe",
}));
std::filesystem::path github_desktop = firstExisting({
local / "GitHubDesktop/GitHubDesktop.exe",
local / "GitHub Desktop/GitHubDesktop.exe",
});
if (github_desktop.empty())
github_desktop = findExecutable(local / "GitHubDesktop", "GitHubDesktop.exe");
add(ExternalApplicationId::github_desktop, "GitHub Desktop", std::move(github_desktop));
add(ExternalApplicationId::file_explorer, "File Explorer",
firstExisting({windows / "explorer.exe"}));
add(ExternalApplicationId::terminal, "Terminal",
firstExisting({local / "Microsoft/WindowsApps/wt.exe", windows / "System32/wt.exe"}), {"-d"});
add(ExternalApplicationId::git_bash, "Git Bash", firstExisting({
program_files / "Git/git-bash.exe", program_files_x86 / "Git/git-bash.exe",
}), {"--cd="});
add(ExternalApplicationId::wsl, "WSL", firstExisting({windows / "System32/wsl.exe"}), {"--cd"});
add(ExternalApplicationId::android_studio, "Android Studio", firstExisting({
program_files / "Android/Android Studio/bin/studio64.exe",
local / "Programs/Android Studio/bin/studio64.exe",
}));
std::filesystem::path idea = firstExisting({
program_files / "JetBrains/IntelliJ IDEA 2025.1/bin/idea64.exe",
program_files / "JetBrains/IntelliJ IDEA 2024.3/bin/idea64.exe",
local / "Programs/IntelliJ IDEA Ultimate/bin/idea64.exe",
});
if (idea.empty()) idea = findExecutable(local / "JetBrains/Toolbox/apps", "idea64.exe");
add(ExternalApplicationId::intellij_idea, "IntelliJ IDEA", std::move(idea));
}
ExternalApplicationId ApplicationManager::defaultApplication() const {
const auto available = std::find_if(targets_.begin(), targets_.end(),
[](const LaunchTarget& target) { return target.info.available; });
return available == targets_.end()
? ExternalApplicationId::file_explorer
: available->info.id;
}
bool ApplicationManager::launch(ExternalApplicationId application,
const std::filesystem::path& repository, std::string& error) const {
const auto found = std::find_if(targets_.begin(), targets_.end(),
[application](const LaunchTarget& target) { return target.info.id == application; });
if (found == targets_.end() || !found->info.available) {
error = "The selected application is not installed";
return false;
}
std::vector<std::string> arguments = found->arguments;
const std::string repository_path = repository.string();
if (application == ExternalApplicationId::git_bash && !arguments.empty())
arguments.back() += repository_path;
else
arguments.push_back(repository_path);
const izo::ProcessResult result = izo::LaunchProcess({
found->executable, std::move(arguments), repository, true,
});
if (!result) {
error = result.errorMessage.empty() ? "Unable to launch application" : result.errorMessage;
return false;
}
error = "Opened repository in " + found->info.name;
return true;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
enum class ExternalApplicationId {
visual_studio_code,
visual_studio,
antigravity,
github_desktop,
file_explorer,
terminal,
git_bash,
wsl,
android_studio,
intellij_idea,
};
struct ExternalApplication {
ExternalApplicationId id;
std::string name;
bool available = false;
};
class ApplicationManager {
public:
ApplicationManager();
const std::vector<ExternalApplication>& applications() const { return applications_; }
ExternalApplicationId defaultApplication() const;
bool launch(ExternalApplicationId application, const std::filesystem::path& repository,
std::string& error) const;
private:
struct LaunchTarget {
ExternalApplication info;
std::filesystem::path executable;
std::vector<std::string> arguments;
};
std::vector<LaunchTarget> targets_;
std::vector<ExternalApplication> applications_;
};

View File

@@ -19,6 +19,21 @@ void addBadge(RepositoryView& repository, const git_oid* oid, RefBadge badge) {
if (commit != repository.commits.end()) commit->refs.push_back(std::move(badge));
}
bool remoteIsCoveredByLocalUpstream(const RepositoryView& repository, const git_oid* oid,
const std::string& remote_name) {
if (!oid) return false;
const size_t slash = remote_name.find('/');
const std::string local_name = slash == std::string::npos
? remote_name
: remote_name.substr(slash + 1);
const auto commit = std::find_if(repository.commits.begin(), repository.commits.end(),
[oid](const CommitInfo& item) { return git_oid_equal(&item.oid, oid) != 0; });
if (commit == repository.commits.end()) return false;
return std::any_of(commit->refs.begin(), commit->refs.end(), [&local_name](const RefBadge& badge) {
return badge.kind == RefKind::local && badge.upstream && badge.name == local_name;
});
}
int submoduleCallback(git_submodule* submodule, const char*, void* payload) {
static_cast<std::vector<std::string>*>(payload)->emplace_back(git_submodule_name(submodule));
return 0;
@@ -137,7 +152,10 @@ void GitManager::loadRefBadges(RepositoryView& repository) {
}
git_object* object = nullptr;
if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0) {
addBadge(repository, git_object_id(object), std::move(badge));
const git_oid* oid = git_object_id(object);
if (type != GIT_BRANCH_REMOTE ||
!remoteIsCoveredByLocalUpstream(repository, oid, branch_name))
addBadge(repository, oid, std::move(badge));
git_object_free(object);
}
}
@@ -416,6 +434,57 @@ bool GitManager::captureGit(RepositoryView& repository, const std::vector<std::s
return true;
}
bool GitManager::prepareCredentials(RepositoryView& repository, std::string& error) {
if (repository.credentials_checked) return true;
bool uses_https = false;
for (const std::string& remote_name : repository.remotes) {
git_remote* remote = nullptr;
if (git_remote_lookup(&remote, repository.repo, remote_name.c_str()) == 0) {
const char* url = git_remote_url(remote);
const std::string remote_url = url ? url : "";
uses_https = uses_https || remote_url.starts_with("https://") || remote_url.starts_with("http://");
git_remote_free(remote);
}
}
if (!uses_https) {
repository.credentials_checked = true;
return true;
}
std::string helper;
std::string helper_error;
const bool has_helper = captureGit(repository, {"config", "--get-all", "credential.helper"},
helper, helper_error) && !helper.empty();
std::string gcm_version;
std::string gcm_error;
const bool gcm_available = captureGit(repository, {"credential-manager", "--version"},
gcm_version, gcm_error);
if (has_helper) {
if (helper.find("manager") != std::string::npos && !gcm_available) {
error = "Git Credential Manager is configured but unavailable. Reinstall Git for Windows with GCM enabled.";
return false;
}
repository.credentials_checked = true;
return true;
}
if (!gcm_available) {
error = "This HTTPS remote needs authentication, but no credential helper is configured. "
"Install Git Credential Manager or configure credential.helper in Git.";
return false;
}
std::string configure_output;
if (!captureGit(repository, {"config", "--local", "credential.helper", "manager"},
configure_output, error))
return false;
repository.credentials_checked = true;
return true;
}
bool GitManager::applyPatch(RepositoryView& repository, const std::string& patch, bool cached,
bool reverse, std::string& error) {
const std::filesystem::path patch_path = std::filesystem::temp_directory_path() /
@@ -437,6 +506,7 @@ bool GitManager::applyPatch(RepositoryView& repository, const std::string& patch
}
bool GitManager::fetch(RepositoryView& repository, const std::string& remote, std::string& error) {
if (!prepareCredentials(repository, error)) return false;
const std::vector<std::string> args = remote.empty()
? std::vector<std::string>{"fetch", "--all", "--prune"}
: std::vector<std::string>{"fetch", remote, "--prune"};
@@ -444,6 +514,7 @@ bool GitManager::fetch(RepositoryView& repository, const std::string& remote, st
}
bool GitManager::pull(RepositoryView& repository, int mode, std::string& error) {
if (!prepareCredentials(repository, error)) return false;
if (mode == 0) return fetch(repository, {}, error);
std::vector<std::string> args{"pull"};
if (mode == 1) args.push_back("--ff");
@@ -453,6 +524,7 @@ bool GitManager::pull(RepositoryView& repository, int mode, std::string& error)
}
bool GitManager::push(RepositoryView& repository, std::string& error) {
if (!prepareCredentials(repository, error)) return false;
if (runGit(repository, {"push"}, "Push complete", error)) return true;
if (repository.remotes.empty()) return false;
const std::string remote = std::find(repository.remotes.begin(), repository.remotes.end(), "origin") !=
@@ -528,6 +600,7 @@ bool GitManager::createTag(RepositoryView& repository, const std::string& name,
bool GitManager::addRemote(RepositoryView& repository, const std::string& name,
const std::string& url, std::string& error) {
repository.credentials_checked = false;
return runGit(repository, {"remote", "add", name, url}, "Remote added", error);
}

View File

@@ -54,6 +54,7 @@ private:
void computeGraphLanes(RepositoryView& repository);
bool loadRepositoryData(RepositoryView& repository, std::string& error);
void loadWorkingTree(RepositoryView& repository);
bool prepareCredentials(RepositoryView& repository, std::string& error);
bool runGit(RepositoryView& repository, const std::vector<std::string>& arguments,
const char* success_message, std::string& message, bool reload = true);
};

View File

@@ -47,6 +47,7 @@ struct RepositoryView {
git_repository* repo = nullptr;
git_revwalk* commit_walk = nullptr;
bool history_exhausted = false;
bool credentials_checked = false;
std::string path;
std::string name = "New Tab";
std::string branch = "detached";
@@ -70,5 +71,6 @@ struct RepositoryView {
commit_walk = nullptr;
if (repo) git_repository_free(repo);
repo = nullptr;
credentials_checked = false;
}
};

View File

@@ -6,6 +6,7 @@
#include <izo/Dialogs.hpp>
#include <IconsFontAwesome6.h>
#include "ui/gitree_ui.h"
#include "managers/application_manager.h"
#include "managers/avatar_cache.h"
#include "managers/git_manager.h"
#include "managers/user_data.h"
@@ -42,7 +43,6 @@ 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;
@@ -52,10 +52,14 @@ std::array<char, 256> g_git_name{};
std::array<char, 1024> g_git_value{};
std::array<char, 1024> g_git_path{};
std::string g_git_target;
std::array<char, 256> g_inline_branch_name{};
std::string g_inline_branch_target;
RepositoryView* g_inline_branch_repository = nullptr;
int g_inline_branch_commit = -1;
bool g_focus_inline_branch = false;
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<char, 128> g_commit_summary{};
std::array<char, 1024> g_commit_description{};
@@ -65,6 +69,8 @@ float g_details_width = 368.0f;
DiffViewer g_diff_viewer;
WindowManager* g_window_manager = nullptr;
GitManager* g_git_manager = nullptr;
ApplicationManager* g_application_manager = nullptr;
ExternalApplicationId g_open_in_application = ExternalApplicationId::visual_studio_code;
ImFont* g_outline_icon_font = nullptr;
float g_outline_icon_size = 15.0f;
@@ -76,6 +82,28 @@ constexpr const char* ICON_TB_TAG = "\xee\xa4\x80";
float ui(float value) { return value * g_ui_scale; }
RepositoryView& repo() { return *g_tabs.at(g_active_tab); }
void cancel_inline_branch() {
g_inline_branch_repository = nullptr;
g_inline_branch_commit = -1;
g_inline_branch_target.clear();
g_inline_branch_name.fill('\0');
g_focus_inline_branch = false;
}
void begin_inline_branch(int commit_index) {
if (commit_index < 0 || commit_index >= static_cast<int>(repo().commits.size())) {
g_notice = "Create an initial commit before creating a branch";
return;
}
char target[GIT_OID_SHA1_HEXSIZE + 1]{};
git_oid_tostr(target, sizeof(target), &repo().commits[static_cast<size_t>(commit_index)].oid);
g_inline_branch_repository = &repo();
g_inline_branch_commit = commit_index;
g_inline_branch_target = target;
g_inline_branch_name.fill('\0');
g_focus_inline_branch = true;
}
void persist_repository_session() {
if (!g_user_data || g_tabs.empty()) return;
std::vector<std::string> paths;
@@ -93,6 +121,7 @@ void create_new_tab(bool persist = true) {
void close_tab(size_t index) {
if (index >= g_tabs.size()) return;
if (g_inline_branch_repository == g_tabs[index].get()) cancel_inline_branch();
if (g_user_data && !g_tabs[index]->path.empty()) g_user_data->addRecentlyClosed(g_tabs[index]->path);
g_tabs.erase(g_tabs.begin() + static_cast<std::ptrdiff_t>(index));
if (g_tabs.empty()) create_new_tab();
@@ -291,24 +320,39 @@ bool sidebar_section_header(const char* label, int count, const char* add_toolti
const ImVec2 header_min = ImGui::GetCursorScreenPos();
const float header_width = ImGui::GetContentRegionAvail().x;
const bool open = sidebar_collapse_row(label, label, true, ui(62.0f));
ImGui::SameLine(0, ui(5.0f));
ImGui::TextDisabled("%d", count);
const ImVec2 mouse = ImGui::GetIO().MousePos;
const bool header_hovered = ImGui::IsWindowHovered() && mouse.x >= header_min.x &&
mouse.x <= header_min.x + header_width && mouse.y >= header_min.y &&
mouse.y <= ImGui::GetItemRectMax().y;
mouse.y <= header_min.y + ui(24.0f);
char count_text[16]{};
std::snprintf(count_text, sizeof(count_text), "%d", count);
const ImVec2 count_size = ImGui::CalcTextSize(count_text);
const float button_size = ui(24.0f);
const float button_x = header_min.x + header_width - button_size - ui(4.0f);
const float text_y = header_min.y + (ui(24.0f) - count_size.y) * 0.5f;
ImGui::GetWindowDrawList()->AddText(
{button_x - ui(8.0f) - count_size.x, text_y}, ImGui::GetColorU32(ImGuiCol_TextDisabled), count_text);
if (header_hovered) {
ImGui::SameLine(0, ui(7.0f));
ImGui::PushID(label);
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_CIRCLE_PLUS "##add")) {
ImGui::SetCursorScreenPos({button_x, header_min.y});
const bool clicked = ImGui::InvisibleButton("##add", {button_size, button_size});
const ImVec2 button_min = ImGui::GetItemRectMin();
const ImVec2 button_max = ImGui::GetItemRectMax();
ImDrawList* draw = ImGui::GetWindowDrawList();
draw->AddRectFilled(button_min, button_max,
ImGui::IsItemHovered() ? IM_COL32(35, 72, 48, 255) : IM_COL32(31, 51, 39, 255));
draw->AddRect(button_min, button_max, IM_COL32(82, 184, 103, 255), 0.0f, 0, ui(1.0f));
const ImVec2 center{(button_min.x + button_max.x) * 0.5f, (button_min.y + button_max.y) * 0.5f};
draw->AddLine({center.x - ui(3.5f), center.y}, {center.x + ui(3.5f), center.y},
IM_COL32(151, 218, 164, 255), ui(1.0f));
draw->AddLine({center.x, center.y - ui(3.5f)}, {center.x, center.y + ui(3.5f)},
IM_COL32(151, 218, 164, 255), ui(1.0f));
if (clicked) {
const std::string section = label;
if (section.find("LOCAL") != std::string::npos) {
g_git_target.clear();
g_branch_create_popup = true;
begin_inline_branch(repo().selected_commit >= 0 ? repo().selected_commit : 0);
}
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;
@@ -316,10 +360,9 @@ bool sidebar_section_header(const char* label, int count, const char* add_toolti
else g_notice = add_notice;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("%s", add_tooltip);
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
ImGui::PopID();
}
ImGui::SetCursorScreenPos({header_min.x, header_min.y + ui(24.0f)});
return open;
}
@@ -365,17 +408,18 @@ void sidebar_item_context(const std::string& item, SidebarItemKind kind) {
void section(const char* label, const std::vector<std::string>& items, const char* item_icon,
const char* add_tooltip, const char* add_notice, SidebarItemKind kind, size_t section_index,
float body_height, float maximum_height) {
float body_height, float maximum_height, bool resizable) {
const bool open = sidebar_section_header(label, static_cast<int>(items.size()), add_tooltip, add_notice);
if (open && body_height >= ui(1.0f)) {
ImGui::BeginChild((std::string(label) + "##body").c_str(), {-1, body_height});
const std::vector<std::string> snapshot = items;
for (const auto& item : snapshot) {
if (g_filter[0] && item.find(g_filter.data()) == std::string::npos) continue;
sidebar_item_row(item_icon, item, item);
sidebar_item_context(item, kind);
}
ImGui::EndChild();
sidebar_section_splitter(label, section_index, maximum_height);
if (resizable) sidebar_section_splitter(label, section_index, maximum_height);
} else {
ImGui::Separator();
}
@@ -439,7 +483,7 @@ void draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const
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, bool remote,
size_t section_index, float body_height, float maximum_height) {
size_t section_index, float body_height, float maximum_height, bool resizable) {
const bool open = sidebar_section_header(label, static_cast<int>(branches.size()), add_tooltip, add_notice);
if (open && body_height >= ui(1.0f)) {
ImGui::BeginChild((std::string(label) + "##body").c_str(), {-1, body_height});
@@ -452,7 +496,7 @@ void branch_section(const char* label, const std::vector<std::string>& branches,
draw_branch_nodes(root, branch_icon, group_icon, remote, label);
ImGui::Unindent(ui(17.0f));
ImGui::EndChild();
sidebar_section_splitter(label, section_index, maximum_height);
if (resizable) sidebar_section_splitter(label, section_index, maximum_height);
} else {
ImGui::Separator();
}
@@ -462,27 +506,12 @@ void draw_sidebar(float width) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(39 / 255.0f, 42 / 255.0f, 49 / 255.0f, 1.0f));
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;
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();
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<int>(repo().worktrees.size() + repo().submodules.size())
: static_cast<int>(repo().local_branches.size() + repo().remote_branches.size());
const int viewing_count = static_cast<int>(repo().local_branches.size() + repo().remote_branches.size() +
repo().worktrees.size() + repo().submodules.size());
ImGui::TextDisabled(ICON_FA_EYE " VIEWING %d", viewing_count);
ImGui::SetNextItemWidth(-1);
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::InputTextWithHint("##filter", ICON_FA_MAGNIFYING_GLASS " Search sidebar...",
g_filter.data(), g_filter.size());
ImGui::Spacing();
ImGui::BeginChild("sidebar_sections", {-1, -1}, ImGuiChildFlags_None,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
@@ -496,61 +525,54 @@ void draw_sidebar(float width) {
ICON_FA_CUBES " SUBMODULES",
};
std::array<bool, 4> section_open{};
int open_count = 0;
std::vector<size_t> open_indices;
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;
if (section_open[index]) open_indices.push_back(index);
}
const float header_space = (ui(24.0f) + 1.0f) * 2.0f;
const float splitter_space = ui(5.0f) * open_count;
const size_t last_open = open_indices.empty() ? section_open.size() : open_indices.back();
const float header_space = (ui(24.0f) + 1.0f) * static_cast<float>(section_open.size());
const float splitter_space = open_indices.size() > 1
? ui(5.0f) * static_cast<float>(open_indices.size() - 1)
: 0.0f;
const float body_space = std::max(0.0f, ImGui::GetContentRegionAvail().y - header_space - splitter_space);
std::array<float, 4> section_heights{};
std::array<float, 4> maximum_heights{};
float requested_space = 0.0f;
for (size_t index = 0; index < section_heights.size(); ++index) {
if (!section_open[index]) continue;
section_heights[index] = ui(g_user_data->sidebarSectionHeight(index));
requested_space += section_heights[index];
}
float overflow = std::max(0.0f, requested_space - body_space);
for (size_t index = section_heights.size(); index-- > 0 && overflow > 0.0f;) {
if (!section_open[index]) continue;
const float reduction = std::min(overflow, std::max(0.0f, section_heights[index] - ui(1.0f)));
section_heights[index] -= reduction;
overflow -= reduction;
}
const float logical_body_space = body_space / g_ui_scale;
for (size_t index = 0; index < maximum_heights.size(); ++index) {
if (!section_open[index]) continue;
float other_heights = 0.0f;
for (size_t other = 0; other < section_heights.size(); ++other) {
if (other != index && section_open[other])
other_heights += g_user_data->sidebarSectionHeight(other);
if (!open_indices.empty()) {
const float minimum_body = std::min(ui(42.0f), body_space / static_cast<float>(open_indices.size()));
float remaining = body_space;
for (size_t position = 0; position + 1 < open_indices.size(); ++position) {
const size_t index = open_indices[position];
const size_t remaining_sections = open_indices.size() - position - 1;
const float maximum = std::max(minimum_body,
remaining - minimum_body * static_cast<float>(remaining_sections));
section_heights[index] = std::clamp(
ui(g_user_data->sidebarSectionHeight(index)), minimum_body, maximum);
remaining -= section_heights[index];
}
section_heights[last_open] = std::max(0.0f, remaining);
for (size_t position = 0; position + 1 < open_indices.size(); ++position) {
const size_t index = open_indices[position];
float other_heights = 0.0f;
for (size_t other_position = 0; other_position + 1 < open_indices.size(); ++other_position)
if (other_position != position) other_heights += section_heights[open_indices[other_position]];
maximum_heights[index] = std::max(minimum_body,
body_space - other_heights - minimum_body) / g_ui_scale;
}
maximum_heights[index] = std::max(1.0f, logical_body_space - other_heights);
}
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]);
}
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]);
}
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],
section_open[0] && last_open != 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],
section_open[1] && last_open != 1);
section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees, ICON_FA_COMPUTER,
"Add worktree", "Add worktree", SidebarItemKind::worktree, 2,
section_heights[2], maximum_heights[2], section_open[2] && last_open != 2);
section(ICON_FA_CUBES " SUBMODULES", repo().submodules, ICON_FA_CUBES,
"Add submodule", "Add submodule", SidebarItemKind::submodule, 3,
section_heights[3], maximum_heights[3], false);
ImGui::PopStyleVar();
ImGui::EndChild();
ImGui::EndChild();
@@ -576,7 +598,7 @@ float ref_badge_width(const RefBadge& badge) {
const std::string display_name = ref_display_name(badge);
float width = ImGui::CalcTextSize(display_name.c_str()).x + ui(12.0f);
if (badge.current) width += outline_icon_width(ICON_TB_CHECK) + ui(5.0f);
if (badge.worktree) width += ui(5.0f) + outline_icon_width(ICON_TB_DEVICE_LAPTOP);
if (badge.kind == RefKind::local) width += ui(5.0f) + outline_icon_width(ICON_TB_DEVICE_LAPTOP);
if (badge.kind == RefKind::remote || badge.upstream)
width += ui(5.0f) + outline_icon_width(ICON_TB_CLOUD);
if (badge.kind == RefKind::tag) width += ui(5.0f) + outline_icon_width(ICON_TB_TAG);
@@ -612,7 +634,7 @@ void draw_ref_badge(const RefBadge& badge, int index, int lane) {
if (badge.current) draw_icon(ICON_TB_CHECK);
draw->AddText({x, text_y}, color, display_name.c_str());
x += label_size.x + ui(5.0f);
if (badge.worktree) draw_icon(ICON_TB_DEVICE_LAPTOP);
if (badge.kind == RefKind::local) draw_icon(ICON_TB_DEVICE_LAPTOP);
if (show_cloud) draw_icon(ICON_TB_CLOUD);
if (show_tag) draw_icon(ICON_TB_TAG);
@@ -680,6 +702,8 @@ void draw_commit_table() {
}
}
row_heights[index] = std::max(ui(24.0f), lines * ui(23.0f) + ui(1.0f));
if (g_inline_branch_repository == &repo() && g_inline_branch_commit == static_cast<int>(index))
row_heights[index] = std::max(row_heights[index], ui(30.0f));
}
ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
@@ -713,7 +737,9 @@ void draw_commit_table() {
const std::string change_count = std::string(ICON_FA_CIRCLE_PLUS " ") +
std::to_string(repo().working_files.size());
const float count_width = ImGui::CalcTextSize(change_count.c_str()).x;
ImGui::SetNextItemWidth(std::max(ui(90.0f), ImGui::GetContentRegionAvail().x - count_width - ui(12.0f)));
const float pending_width = std::clamp(
ImGui::GetContentRegionAvail().x - count_width - ui(12.0f), ui(90.0f), ui(180.0f));
ImGui::SetNextItemWidth(pending_width);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(7.0f), ui(1.0f)});
ImGui::InputTextWithHint("##pending_commit_summary", "// WIP",
g_commit_summary.data(), g_commit_summary.size());
@@ -722,6 +748,7 @@ void draw_commit_table() {
ImGui::TextColored(ImVec4(0.38f, 0.84f, 0.48f, 1), "%s",
change_count.c_str());
}
bool submit_inline_branch = false;
for (int i = 0; i < static_cast<int>(repo().commits.size()); ++i) {
const auto& commit = repo().commits[i];
if (!commit_visible(commit)) continue;
@@ -749,23 +776,37 @@ void draw_commit_table() {
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;
begin_inline_branch(i);
}
ImGui::EndPopup();
}
float chip_x = reference_origin.x + ui(3.0f);
float chip_y = reference_origin.y + ui(0.5f);
const float chip_right = reference_origin.x + chip_line_width;
if (!commit.refs.empty()) {
const bool editing_branch = g_inline_branch_repository == &repo() && g_inline_branch_commit == i;
if (editing_branch) {
ImGui::SetCursorScreenPos({reference_origin.x + ui(3.0f), reference_origin.y + ui(1.0f)});
ImGui::SetNextItemWidth(std::clamp(chip_line_width - ui(4.0f), ui(90.0f), ui(170.0f)));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.08f, 0.10f, 0.14f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.16f, 0.48f, 0.95f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, ui(1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(7.0f), ui(3.0f)});
if (g_focus_inline_branch) {
ImGui::SetKeyboardFocusHere();
g_focus_inline_branch = false;
}
submit_inline_branch = ImGui::InputTextWithHint("##inline_branch_name", "enter branch name",
g_inline_branch_name.data(), g_inline_branch_name.size(), ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(2);
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) cancel_inline_branch();
} else if (!commit.refs.empty()) {
const float connector_y = reference_origin.y + row_height * 0.5f;
ImGui::GetWindowDrawList()->AddLine(
{reference_origin.x, connector_y}, {chip_right + ui(6.0f), connector_y},
GraphRenderer::laneColor(commit.lane, 120), ui(1.0f));
}
for (int ref_index = 0; ref_index < static_cast<int>(commit.refs.size()); ++ref_index) {
for (int ref_index = 0; !editing_branch && ref_index < static_cast<int>(commit.refs.size()); ++ref_index) {
const float badge_width = ref_badge_width(commit.refs[ref_index]);
if (chip_x > reference_origin.x + ui(3.0f) && chip_x + badge_width > chip_right) {
chip_x = reference_origin.x + ui(3.0f);
@@ -778,7 +819,13 @@ void draw_commit_table() {
ImGui::TableSetColumnIndex(1);
graph.drawRow(i, commit, repo().commits, row_heights, parent_rows, g_avatar_cache);
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(commit.summary.c_str());
ImGui::PushID(i);
const ImVec2 message_position = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton("##commit_message", {-1, std::max(ImGui::GetTextLineHeight(), row_height - ui(2.0f))});
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) repo().selected_commit = i;
ImGui::GetWindowDrawList()->AddText(message_position, ImGui::GetColorU32(ImGuiCol_Text),
commit.summary.c_str());
ImGui::PopID();
ImGui::TableSetColumnIndex(3);
ImGui::TextDisabled("%s", commit.date.c_str());
}
@@ -787,6 +834,12 @@ void draw_commit_table() {
ImGui::EndTable();
ImGui::PopStyleVar();
if (load_more_history) g_git_manager->loadMoreCommits(repo(), 500, g_notice);
if (submit_inline_branch && g_inline_branch_name[0] != '\0') {
const std::string name = g_inline_branch_name.data();
const std::string target = g_inline_branch_target;
if (g_git_manager->createBranch(repo(), name, target, true, g_notice)) cancel_inline_branch();
else g_focus_inline_branch = true;
}
}
const char* change_icon(FileChangeKind kind) {
@@ -916,14 +969,6 @@ void draw_working_details() {
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)})) {
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();
@@ -1005,19 +1050,11 @@ void draw_working_details() {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(31 / 255.0f, 34 / 255.0f, 39 / 255.0f, 1.0f));
ImGui::BeginChild("commit_message_card", {-1, ui(165.0f)}, ImGuiChildFlags_Borders);
const float summary_controls_width = ui(60.0f);
const float summary_controls_width = ui(30.0f);
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - summary_controls_width);
ImGui::InputTextWithHint("##commit_summary", "Commit summary", g_commit_summary.data(), g_commit_summary.size());
ImGui::SameLine();
ImGui::TextDisabled("72");
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")) {
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(),
{-1, -1});
@@ -1031,17 +1068,6 @@ void draw_working_details() {
ImGui::PopStyleColor();
ImGui::TextDisabled(ICON_FA_CHEVRON_RIGHT " Commit options");
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")) {
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));
@@ -1223,6 +1249,71 @@ bool toolbar_selector(const char* id, const char* label, const std::string& valu
return clicked;
}
const char* application_icon(ExternalApplicationId application) {
switch (application) {
case ExternalApplicationId::visual_studio_code: return ICON_FA_CODE;
case ExternalApplicationId::visual_studio: return ICON_FA_CUBES;
case ExternalApplicationId::antigravity: return ICON_FA_WINDOW_MAXIMIZE;
case ExternalApplicationId::github_desktop: return ICON_FA_CIRCLE_NODES;
case ExternalApplicationId::file_explorer: return ICON_FA_FOLDER;
case ExternalApplicationId::terminal: return ICON_FA_TERMINAL;
case ExternalApplicationId::git_bash: return ICON_FA_CODE_BRANCH;
case ExternalApplicationId::wsl: return ICON_FA_SERVER;
case ExternalApplicationId::android_studio: return ICON_FA_ROBOT;
case ExternalApplicationId::intellij_idea: return ICON_FA_JET_FIGHTER_UP;
}
return ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE;
}
void open_repository_in(ExternalApplicationId application) {
if (!g_application_manager) return;
g_open_in_application = application;
g_application_manager->launch(application, repo().path, g_notice);
}
void draw_open_in_button() {
if (!g_application_manager) return;
constexpr float button_width = 58.0f;
ImGui::SetCursorPos({ImGui::GetWindowWidth() - ui(button_width + 7.0f), ui(5.0f)});
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0, 0});
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(5.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, ui(1.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.14f, 0.17f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.21f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.25f, 0.28f, 0.33f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.20f, 0.68f, 0.95f, 1.0f));
if (ImGui::Button((std::string(application_icon(g_open_in_application)) + "##open_in").c_str(),
{ui(37.0f), ui(31.0f)}))
open_repository_in(g_open_in_application);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("Open repository");
ImGui::SameLine(0, 0);
ImGui::PopStyleColor();
if (ImGui::Button(ICON_FA_CHEVRON_DOWN "##open_in_menu", {ui(21.0f), ui(31.0f)}))
ImGui::OpenPopup("open_repository_in");
ImGui::PopStyleColor(3);
ImGui::PopStyleVar(3);
ImGui::SetNextWindowSize({ui(230.0f), 0}, ImGuiCond_Appearing);
if (ImGui::BeginPopup("open_repository_in")) {
ImGui::TextDisabled("OPEN REPOSITORY IN");
ImGui::Separator();
for (const ExternalApplication& application : g_application_manager->applications()) {
ImGui::PushID(static_cast<int>(application.id));
ImGui::BeginDisabled(!application.available);
const std::string label = std::string(application_icon(application.id)) + " " + application.name;
if (ImGui::MenuItem(label.c_str(), nullptr, application.id == g_open_in_application)) {
open_repository_in(application.id);
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
if (!application.available && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("Not installed");
ImGui::PopID();
}
ImGui::EndPopup();
}
}
bool contains_case_insensitive(const std::string& text, const char* query) {
if (!query || !*query) return true;
std::string lowered_text = text;
@@ -1311,35 +1402,6 @@ void draw_licenses_popup() {
}
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;
@@ -1724,8 +1786,7 @@ void draw_app() {
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_git_target.clear();
g_branch_create_popup = true;
begin_inline_branch(repo().selected_commit >= 0 ? repo().selected_commit : 0);
}
ImGui::SameLine(0, action_spacing);
if (toolbar_action("stash", "Stash", ICON_FA_BOX_ARCHIVE, "Stash changes", true, false, 58))
@@ -1733,6 +1794,7 @@ void draw_app() {
ImGui::SameLine(0, action_spacing);
if (toolbar_action("pop", "Pop", ICON_FA_BOX_OPEN, "Pop latest stash", true, false, 54))
g_git_manager->popStash(repo(), g_notice);
draw_open_in_button();
ImGui::EndChild();
ImGui::PopStyleColor();
@@ -1791,6 +1853,9 @@ void draw_app() {
int runGitree(int argc, char** argv) {
GitManager git_manager;
g_git_manager = &git_manager;
ApplicationManager application_manager;
g_application_manager = &application_manager;
g_open_in_application = application_manager.defaultApplication();
UserData user_data;
g_user_data = &user_data;
g_sidebar_width = user_data.sidebarWidth();
@@ -1844,6 +1909,7 @@ int runGitree(int argc, char** argv) {
avatar_cache.shutdown();
g_avatar_cache = nullptr;
g_application_manager = nullptr;
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
user_data.setSidebarWidth(g_sidebar_width);