Merge pull request 'main' (#2) from main into prod
All checks were successful
Build Windows EXE / build-windows (push) Successful in 23m29s
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:
@@ -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
103
README.md
@@ -1,42 +1,93 @@
|
||||
# Gitree
|
||||
|
||||
A native Dear ImGui Git client built with libgit2 and GLFW.
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/actions?workflow=windows-build.yml)
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/releases/latest)
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/releases)
|
||||
[](https://en.cppreference.com/w/cpp/20)
|
||||
[](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).
|
||||
|
||||
122
src/managers/application_manager.cpp
Normal file
122
src/managers/application_manager.cpp
Normal 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;
|
||||
}
|
||||
44
src/managers/application_manager.h
Normal file
44
src/managers/application_manager.h
Normal 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_;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user