feat(app): polish repository workflows and graph

This commit is contained in:
2026-06-18 21:25:51 -05:00
parent 084b707776
commit 0152667b65
19 changed files with 2361 additions and 821 deletions

View File

@@ -38,11 +38,22 @@ jobs:
run: | run: |
cmake -S . -B build-win -G Ninja \ cmake -S . -B build-win -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=mingw-toolchain.cmake \ -DCMAKE_TOOLCHAIN_FILE=mingw-toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++"
- name: Build Windows executable - name: Build Windows executable
run: cmake --build build-win --parallel run: cmake --build build-win --parallel
- name: Verify static runtime linkage
run: |
dependencies="$(x86_64-w64-mingw32-objdump -p build-win/bin/gitree.exe | sed -n 's/.*DLL Name: //p')"
printf '%s\n' "$dependencies"
if printf '%s\n' "$dependencies" | grep -Eqi 'lib(gcc|stdc\+\+|winpthread).*\.dll'; then
echo "The executable still depends on a MinGW runtime DLL."
exit 1
fi
- name: Prepare artifact - name: Prepare artifact
run: | run: |
mkdir -p dist mkdir -p dist

View File

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

View File

@@ -0,0 +1,97 @@
#include "application_icon_cache.h"
#include <GLFW/glfw3.h>
#include <algorithm>
#include <array>
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#endif
ApplicationIconCache::~ApplicationIconCache()
{
shutdown();
}
unsigned int ApplicationIconCache::textureFor(const std::filesystem::path &executable)
{
if (executable.empty())
return 0;
const std::string key = executable.string();
const auto found = textures_.find(key);
if (found != textures_.end())
return found->second;
const unsigned int texture = loadTexture(executable);
textures_.emplace(key, texture);
return texture;
}
unsigned int ApplicationIconCache::loadTexture(const std::filesystem::path &executable) const
{
#ifdef _WIN32
HICON large_icon = nullptr;
HICON small_icon = nullptr;
if (ExtractIconExW(executable.wstring().c_str(), 0, &large_icon, &small_icon, 1) == 0)
return 0;
HICON icon = large_icon ? large_icon : small_icon;
if (!icon)
return 0;
constexpr int size = 32;
BITMAPINFO bitmap_info{};
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmap_info.bmiHeader.biWidth = size;
bitmap_info.bmiHeader.biHeight = -size;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32;
bitmap_info.bmiHeader.biCompression = BI_RGB;
void *pixels = nullptr;
HDC device = CreateCompatibleDC(nullptr);
HBITMAP bitmap = CreateDIBSection(device, &bitmap_info, DIB_RGB_COLORS, &pixels, nullptr, 0);
unsigned int texture = 0;
if (bitmap && pixels)
{
const HGDIOBJ previous = SelectObject(device, bitmap);
std::fill_n(static_cast<unsigned char *>(pixels), size * size * 4, 0);
if (DrawIconEx(device, 0, 0, icon, size, size, 0, nullptr, DI_NORMAL))
{
auto *rgba = static_cast<unsigned char *>(pixels);
for (int pixel = 0; pixel < size * size; ++pixel)
std::swap(rgba[pixel * 4], rgba[pixel * 4 + 2]);
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glBindTexture(GL_TEXTURE_2D, 0);
}
SelectObject(device, previous);
}
if (bitmap)
DeleteObject(bitmap);
if (device)
DeleteDC(device);
if (large_icon)
DestroyIcon(large_icon);
if (small_icon)
DestroyIcon(small_icon);
return texture;
#else
(void)executable;
return 0;
#endif
}
void ApplicationIconCache::shutdown()
{
for (auto &[path, texture] : textures_)
{
(void)path;
if (texture)
glDeleteTextures(1, &texture);
}
textures_.clear();
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <filesystem>
#include <map>
#include <string>
class ApplicationIconCache
{
public:
~ApplicationIconCache();
ApplicationIconCache(const ApplicationIconCache &) = delete;
ApplicationIconCache &operator=(const ApplicationIconCache &) = delete;
ApplicationIconCache() = default;
unsigned int textureFor(const std::filesystem::path &executable);
void shutdown();
private:
unsigned int loadTexture(const std::filesystem::path &executable) const;
std::map<std::string, unsigned int> textures_;
};

View File

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

View File

@@ -4,7 +4,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
enum class ExternalApplicationId { enum class ExternalApplicationId
{
visual_studio_code, visual_studio_code,
visual_studio, visual_studio,
antigravity, antigravity,
@@ -17,25 +18,29 @@ enum class ExternalApplicationId {
intellij_idea, intellij_idea,
}; };
struct ExternalApplication { struct ExternalApplication
{
ExternalApplicationId id; ExternalApplicationId id;
std::string name; std::string name;
bool available = false; bool available = false;
std::filesystem::path executable;
}; };
class ApplicationManager { class ApplicationManager
{
public: public:
ApplicationManager(); ApplicationManager();
const std::vector<ExternalApplication>& applications() const { return applications_; } const std::vector<ExternalApplication> &applications() const { return applications_; }
const ExternalApplication *application(ExternalApplicationId id) const;
ExternalApplicationId defaultApplication() const; ExternalApplicationId defaultApplication() const;
bool launch(ExternalApplicationId application, const std::filesystem::path& repository, bool launch(ExternalApplicationId application, const std::filesystem::path &repository,
std::string& error) const; std::string &error) const;
private: private:
struct LaunchTarget { struct LaunchTarget
{
ExternalApplication info; ExternalApplication info;
std::filesystem::path executable;
std::vector<std::string> arguments; std::vector<std::string> arguments;
}; };

View File

@@ -17,23 +17,31 @@
#include <wincodec.h> #include <wincodec.h>
#endif #endif
AvatarCache::AvatarCache(const std::filesystem::path& user_data_directory) AvatarCache::AvatarCache(const std::filesystem::path &user_data_directory)
: directory_(user_data_directory / "avatars") { : directory_(user_data_directory / "avatars")
{
std::filesystem::create_directories(directory_); std::filesystem::create_directories(directory_);
} }
AvatarCache::~AvatarCache() { AvatarCache::~AvatarCache()
{
shutdown(); shutdown();
} }
std::string AvatarCache::hashEmail(const std::string& email) const { std::string AvatarCache::hashEmail(const std::string &email) const
{
std::string normalized = email; std::string normalized = email;
normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(), normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(),
[](unsigned char value) { return !std::isspace(value); })); [](unsigned char value)
{ return !std::isspace(value); }));
normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(), normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(),
[](unsigned char value) { return !std::isspace(value); }).base(), normalized.end()); [](unsigned char value)
{ return !std::isspace(value); })
.base(),
normalized.end());
std::transform(normalized.begin(), normalized.end(), normalized.begin(), std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char value) { return static_cast<char>(std::tolower(value)); }); [](unsigned char value)
{ return static_cast<char>(std::tolower(value)); });
#ifdef _WIN32 #ifdef _WIN32
BCRYPT_ALG_HANDLE algorithm = nullptr; BCRYPT_ALG_HANDLE algorithm = nullptr;
@@ -43,99 +51,121 @@ std::string AvatarCache::hashEmail(const std::string& email) const {
DWORD written = 0; DWORD written = 0;
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 || if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 ||
BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&object_size), BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&object_size),
sizeof(object_size), &written, 0) < 0) { sizeof(object_size), &written, 0) < 0)
if (algorithm) BCryptCloseAlgorithmProvider(algorithm, 0); {
if (algorithm)
BCryptCloseAlgorithmProvider(algorithm, 0);
return {}; return {};
} }
std::vector<unsigned char> object(object_size); std::vector<unsigned char> object(object_size);
if (BCryptCreateHash(algorithm, &hash, object.data(), object_size, nullptr, 0, 0) < 0 || if (BCryptCreateHash(algorithm, &hash, object.data(), object_size, nullptr, 0, 0) < 0 ||
BCryptHashData(hash, reinterpret_cast<PUCHAR>(normalized.data()), BCryptHashData(hash, reinterpret_cast<PUCHAR>(normalized.data()),
static_cast<ULONG>(normalized.size()), 0) < 0 || static_cast<ULONG>(normalized.size()), 0) < 0 ||
BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0) { BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0)
if (hash) BCryptDestroyHash(hash); {
if (hash)
BCryptDestroyHash(hash);
BCryptCloseAlgorithmProvider(algorithm, 0); BCryptCloseAlgorithmProvider(algorithm, 0);
return {}; return {};
} }
BCryptDestroyHash(hash); BCryptDestroyHash(hash);
BCryptCloseAlgorithmProvider(algorithm, 0); BCryptCloseAlgorithmProvider(algorithm, 0);
std::ostringstream output; std::ostringstream output;
for (unsigned char value : digest) output << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(value); for (unsigned char value : digest)
output << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(value);
return output.str(); return output.str();
#else #else
return std::to_string(std::hash<std::string>{}(normalized)); return std::to_string(std::hash<std::string>{}(normalized));
#endif #endif
} }
unsigned int AvatarCache::textureFor(const std::string& email) { unsigned int AvatarCache::textureFor(const std::string &email)
if (email.empty()) return 0; {
if (email.empty())
return 0;
const std::string hash = hashEmail(email); const std::string hash = hashEmail(email);
if (hash.empty()) return 0; if (hash.empty())
Entry& entry = entries_[hash]; return 0;
if (entry.texture) return entry.texture; Entry &entry = entries_[hash];
if (entry.requested) return 0; if (entry.texture)
return entry.texture;
if (entry.requested)
return 0;
entry.requested = true; entry.requested = true;
entry.file = directory_ / (hash + ".img"); entry.file = directory_ / (hash + ".img");
if (std::filesystem::exists(entry.file)) { if (std::filesystem::exists(entry.file))
{
entry.texture = loadTexture(entry.file); entry.texture = loadTexture(entry.file);
return entry.texture; return entry.texture;
} }
#ifdef _WIN32 #ifdef _WIN32
const std::filesystem::path file = entry.file; const std::filesystem::path file = entry.file;
const std::wstring url = L"https://www.gravatar.com/avatar/" + const std::wstring url = L"https://www.gravatar.com/avatar/" +
std::wstring(hash.begin(), hash.end()) + L"?s=64&d=identicon&r=g"; std::wstring(hash.begin(), hash.end()) + L"?s=64&d=identicon&r=g";
entry.download = std::async(std::launch::async, [file, url] { entry.download = std::async(std::launch::async, [file, url]
return SUCCEEDED(URLDownloadToFileW(nullptr, url.c_str(), file.wstring().c_str(), 0, nullptr)); { return SUCCEEDED(URLDownloadToFileW(nullptr, url.c_str(), file.wstring().c_str(), 0, nullptr)); });
});
#endif #endif
return 0; return 0;
} }
void AvatarCache::update() { void AvatarCache::update()
for (auto& [hash, entry] : entries_) { {
for (auto &[hash, entry] : entries_)
{
(void)hash; (void)hash;
if (entry.texture || !entry.download.valid()) continue; if (entry.texture || !entry.download.valid())
if (entry.download.wait_for(std::chrono::seconds(0)) != std::future_status::ready) continue; continue;
if (entry.download.get()) entry.texture = loadTexture(entry.file); if (entry.download.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
continue;
if (entry.download.get())
entry.texture = loadTexture(entry.file);
} }
} }
unsigned int AvatarCache::loadTexture(const std::filesystem::path& file) const { unsigned int AvatarCache::loadTexture(const std::filesystem::path &file) const
{
#ifdef _WIN32 #ifdef _WIN32
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
IWICImagingFactory* factory = nullptr; IWICImagingFactory *factory = nullptr;
IWICBitmapDecoder* decoder = nullptr; IWICBitmapDecoder *decoder = nullptr;
IWICBitmapFrameDecode* frame = nullptr; IWICBitmapFrameDecode *frame = nullptr;
IWICFormatConverter* converter = nullptr; IWICFormatConverter *converter = nullptr;
UINT width = 0; UINT width = 0;
UINT height = 0; UINT height = 0;
std::vector<unsigned char> pixels; std::vector<unsigned char> pixels;
unsigned int texture = 0; unsigned int texture = 0;
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory))) && IID_PPV_ARGS(&factory))) &&
SUCCEEDED(factory->CreateDecoderFromFilename(file.wstring().c_str(), nullptr, GENERIC_READ, SUCCEEDED(factory->CreateDecoderFromFilename(file.wstring().c_str(), nullptr, GENERIC_READ,
WICDecodeMetadataCacheOnLoad, &decoder)) && WICDecodeMetadataCacheOnLoad, &decoder)) &&
SUCCEEDED(decoder->GetFrame(0, &frame)) && SUCCEEDED(decoder->GetFrame(0, &frame)) &&
SUCCEEDED(factory->CreateFormatConverter(&converter)) && SUCCEEDED(factory->CreateFormatConverter(&converter)) &&
SUCCEEDED(converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA, SUCCEEDED(converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA,
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) && WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) &&
SUCCEEDED(converter->GetSize(&width, &height))) { SUCCEEDED(converter->GetSize(&width, &height)))
{
pixels.resize(static_cast<size_t>(width) * height * 4); pixels.resize(static_cast<size_t>(width) * height * 4);
if (SUCCEEDED(converter->CopyPixels(nullptr, width * 4, if (SUCCEEDED(converter->CopyPixels(nullptr, width * 4,
static_cast<UINT>(pixels.size()), pixels.data()))) { static_cast<UINT>(pixels.size()), pixels.data())))
{
glGenTextures(1, &texture); glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture); glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(width), static_cast<int>(height), glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(width), static_cast<int>(height),
0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
} }
if (converter) converter->Release(); if (converter)
if (frame) frame->Release(); converter->Release();
if (decoder) decoder->Release(); if (frame)
if (factory) factory->Release(); frame->Release();
if (decoder)
decoder->Release();
if (factory)
factory->Release();
CoUninitialize(); CoUninitialize();
return texture; return texture;
#else #else
@@ -144,11 +174,15 @@ unsigned int AvatarCache::loadTexture(const std::filesystem::path& file) const {
#endif #endif
} }
void AvatarCache::shutdown() { void AvatarCache::shutdown()
for (auto& [hash, entry] : entries_) { {
for (auto &[hash, entry] : entries_)
{
(void)hash; (void)hash;
if (entry.download.valid()) entry.download.wait(); if (entry.download.valid())
if (entry.texture) glDeleteTextures(1, &entry.texture); entry.download.wait();
if (entry.texture)
glDeleteTextures(1, &entry.texture);
entry.texture = 0; entry.texture = 0;
} }
entries_.clear(); entries_.clear();

View File

@@ -5,28 +5,30 @@
#include <map> #include <map>
#include <string> #include <string>
class AvatarCache { class AvatarCache
{
public: public:
explicit AvatarCache(const std::filesystem::path& user_data_directory); explicit AvatarCache(const std::filesystem::path &user_data_directory);
~AvatarCache(); ~AvatarCache();
AvatarCache(const AvatarCache&) = delete; AvatarCache(const AvatarCache &) = delete;
AvatarCache& operator=(const AvatarCache&) = delete; AvatarCache &operator=(const AvatarCache &) = delete;
unsigned int textureFor(const std::string& email); unsigned int textureFor(const std::string &email);
void update(); void update();
void shutdown(); void shutdown();
private: private:
struct Entry { struct Entry
{
unsigned int texture = 0; unsigned int texture = 0;
bool requested = false; bool requested = false;
std::filesystem::path file; std::filesystem::path file;
std::future<bool> download; std::future<bool> download;
}; };
std::string hashEmail(const std::string& email) const; std::string hashEmail(const std::string &email) const;
unsigned int loadTexture(const std::filesystem::path& file) const; unsigned int loadTexture(const std::filesystem::path &file) const;
std::filesystem::path directory_; std::filesystem::path directory_;
std::map<std::string, Entry> entries_; std::map<std::string, Entry> entries_;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -4,57 +4,60 @@
#include <string> #include <string>
#include <vector> #include <vector>
class GitManager { class GitManager
{
public: public:
GitManager(); GitManager();
~GitManager(); ~GitManager();
bool openRepository(RepositoryView& repository, const std::string& path, std::string& error); bool openRepository(RepositoryView &repository, const std::string &path, std::string &error);
bool initializeRepository(RepositoryView& repository, const std::string& path, std::string& error); bool initializeRepository(RepositoryView &repository, const std::string &path, std::string &error);
bool reload(RepositoryView& repository, std::string& error); bool reload(RepositoryView &repository, std::string &error);
bool loadMoreCommits(RepositoryView& repository, size_t page_size, std::string& error); bool loadMoreCommits(RepositoryView &repository, size_t page_size, std::string &error);
bool checkoutBranch(RepositoryView& repository, const std::string& branch, std::string& error); bool checkoutBranch(RepositoryView &repository, const std::string &branch, std::string &error);
bool fetch(RepositoryView& repository, const std::string& remote, std::string& error); bool fetch(RepositoryView &repository, const std::string &remote, std::string &error);
bool pull(RepositoryView& repository, int mode, std::string& error); bool pull(RepositoryView &repository, int mode, std::string &error);
bool push(RepositoryView& repository, std::string& error); bool push(RepositoryView &repository, std::string &error);
bool stash(RepositoryView& repository, std::string& error); bool stash(RepositoryView &repository, std::string &error);
bool popStash(RepositoryView& repository, std::string& error); bool popStash(RepositoryView &repository, std::string &error);
bool undoCommit(RepositoryView& repository, std::string& error); bool undoCommit(RepositoryView &repository, std::string &error);
bool redoCommit(RepositoryView& repository, std::string& error); bool redoCommit(RepositoryView &repository, std::string &error);
bool stageAll(RepositoryView& repository, std::string& error); bool stageAll(RepositoryView &repository, std::string &error);
bool stageFile(RepositoryView& repository, const std::string& path, std::string& error); bool stageFile(RepositoryView &repository, const std::string &path, std::string &error);
bool unstageFile(RepositoryView& repository, const std::string& path, std::string& error); bool unstageFile(RepositoryView &repository, const std::string &path, std::string &error);
bool discardFile(RepositoryView& repository, const std::string& path, std::string& error); bool discardFile(RepositoryView &repository, const std::string &path, std::string &error);
bool discardAll(RepositoryView& repository, std::string& error); bool discardAll(RepositoryView &repository, std::string &error);
bool commit(RepositoryView& repository, const std::string& summary, bool commit(RepositoryView &repository, const std::string &summary,
const std::string& description, bool amend, std::string& error); const std::string &description, bool amend, std::string &error);
bool createBranch(RepositoryView& repository, const std::string& name, bool createBranch(RepositoryView &repository, const std::string &name,
const std::string& start_point, bool checkout, std::string& error); const std::string &start_point, bool checkout, std::string &error);
bool createTag(RepositoryView& repository, const std::string& name, bool createTag(RepositoryView &repository, const std::string &name,
const std::string& target, std::string& error); const std::string &target, std::string &error);
bool addRemote(RepositoryView& repository, const std::string& name, bool addRemote(RepositoryView &repository, const std::string &name,
const std::string& url, std::string& error); const std::string &url, std::string &error);
bool addWorktree(RepositoryView& repository, const std::string& path, bool addWorktree(RepositoryView &repository, const std::string &path,
const std::string& branch, std::string& error); const std::string &branch, std::string &error);
bool addSubmodule(RepositoryView& repository, const std::string& url, bool addSubmodule(RepositoryView &repository, const std::string &url,
const std::string& path, std::string& error); const std::string &path, std::string &error);
bool updateSubmodule(RepositoryView& repository, const std::string& name, std::string& error); bool updateSubmodule(RepositoryView &repository, const std::string &name, std::string &error);
std::string worktreePath(RepositoryView& repository, const std::string& name, std::string& error); std::string worktreePath(RepositoryView &repository, const std::string &name, std::string &error);
bool loadCommitChanges(RepositoryView& repository, int commit_index, std::string& error); bool loadCommitChanges(RepositoryView &repository, int commit_index, std::string &error);
bool captureGit(RepositoryView& repository, const std::vector<std::string>& arguments, bool loadCommitFiles(RepositoryView &repository, int commit_index, std::string &error);
std::string& output, std::string& error); bool captureGit(RepositoryView &repository, const std::vector<std::string> &arguments,
bool applyPatch(RepositoryView& repository, const std::string& patch, bool cached, std::string &output, std::string &error);
bool reverse, std::string& error); bool applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
bool reverse, std::string &error);
private: private:
static std::string lastError(const char* fallback); static std::string lastError(const char *fallback);
static std::string formatTime(git_time_t value, int offset_minutes); static std::string formatTime(git_time_t value, int offset_minutes);
void readBranches(RepositoryView& repository, git_branch_t type, std::vector<std::string>& output); void readBranches(RepositoryView &repository, git_branch_t type, std::vector<std::string> &output);
void loadRefBadges(RepositoryView& repository); void loadBranchDivergence(RepositoryView &repository);
void computeGraphLanes(RepositoryView& repository); void loadRefBadges(RepositoryView &repository);
bool loadRepositoryData(RepositoryView& repository, std::string& error); void computeGraphLanes(RepositoryView &repository);
void loadWorkingTree(RepositoryView& repository); bool loadRepositoryData(RepositoryView &repository, std::string &error);
bool prepareCredentials(RepositoryView& repository, std::string& error); void loadWorkingTree(RepositoryView &repository);
bool runGit(RepositoryView& repository, const std::vector<std::string>& arguments, bool prepareCredentials(RepositoryView &repository, std::string &error);
const char* success_message, std::string& message, bool reload = true); bool runGit(RepositoryView &repository, const std::vector<std::string> &arguments,
const char *success_message, std::string &message, bool reload = true);
}; };

View File

@@ -1,72 +1,88 @@
#include "user_data.h" #include "user_data.h"
extern "C" { #include <izo/Paths.hpp>
extern "C"
{
#include <ikv.h> #include <ikv.h>
} }
#include <algorithm> #include <algorithm>
#include <cstdlib>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <utility> #include <utility>
namespace { namespace
std::filesystem::path roaming_directory() { {
#ifdef _WIN32 std::filesystem::path roaming_directory()
if (const char* appdata = std::getenv("APPDATA")) return appdata; {
#endif if (const auto config = izo::GetKnownPath(izo::KnownPath::Config); !config.empty())
if (const char* home = std::getenv("HOME")) return std::filesystem::path(home) / ".config"; return config;
return std::filesystem::temp_directory_path(); if (const auto temporary = izo::GetKnownPath(izo::KnownPath::Temporary); !temporary.empty())
return temporary;
return std::filesystem::temp_directory_path();
}
const ikv_node_t *object_value(const ikv_node_t *object, const char *key, ikv_type_t type)
{
const ikv_node_t *value = object ? ikv_object_get(object, key) : nullptr;
return value && ikv_node_type(value) == type ? value : nullptr;
}
} }
UserData::UserData()
const ikv_node_t* object_value(const ikv_node_t* object, const char* key, ikv_type_t type) { {
const ikv_node_t* value = object ? ikv_object_get(object, key) : nullptr;
return value && ikv_node_type(value) == type ? value : nullptr;
}
}
UserData::UserData() {
directory_ = roaming_directory() / "Identity" / "Gitree"; directory_ = roaming_directory() / "Identity" / "Gitree";
std::filesystem::create_directories(directory_); std::filesystem::create_directories(directory_);
imgui_ini_path_ = (directory_ / "imgui.ini").string(); imgui_ini_path_ = (directory_ / "imgui.ini").string();
load(); load();
} }
UserData::~UserData() { UserData::~UserData()
{
save(); save();
} }
void UserData::load() { void UserData::load()
{
const std::filesystem::path data_path = directory_ / "user_data.ikv"; const std::filesystem::path data_path = directory_ / "user_data.ikv";
if (ikv_node_t* root = ikv_parse_file(data_path.string().c_str())) { if (ikv_node_t *root = ikv_parse_file(data_path.string().c_str()))
if (const ikv_node_t* settings = object_value(root, "settings", IKV_OBJECT)) { {
if (const ikv_node_t* value = object_value(settings, "sidebar_width", IKV_FLOAT)) if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
{
if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
sidebar_width_ = static_cast<float>(ikv_as_float(value)); sidebar_width_ = static_cast<float>(ikv_as_float(value));
if (const ikv_node_t* value = object_value(settings, "details_width", IKV_FLOAT)) if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
details_width_ = static_cast<float>(ikv_as_float(value)); details_width_ = static_cast<float>(ikv_as_float(value));
if (const ikv_node_t* value = object_value(settings, "pull_mode", IKV_INT)) if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
pull_mode_ = static_cast<int>(ikv_as_int(value)); pull_mode_ = static_cast<int>(ikv_as_int(value));
if (const ikv_node_t* heights = object_value(settings, "sidebar_sections", IKV_ARRAY)) { if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>( const uint32_t count = std::min<uint32_t>(
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size())); ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
for (uint32_t index = 0; index < count; ++index) for (uint32_t index = 0; index < count; ++index)
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index))); sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
} }
} }
if (const ikv_node_t* history = object_value(root, "recently_closed", IKV_ARRAY)) { if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 12); const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 12);
for (uint32_t index = 0; index < count; ++index) { for (uint32_t index = 0; index < count; ++index)
const char* path = ikv_as_string(ikv_array_get(history, index)); {
if (path && *path) recently_closed_.emplace_back(path); const char *path = ikv_as_string(ikv_array_get(history, index));
if (path && *path)
recently_closed_.emplace_back(path);
} }
} }
if (const ikv_node_t* session = object_value(root, "session", IKV_OBJECT)) { if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
if (const ikv_node_t* active = object_value(session, "active_tab", IKV_INT)) {
if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active))); active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
if (const ikv_node_t* tabs = object_value(session, "tabs", IKV_ARRAY)) { if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index) { {
const char* path = ikv_as_string(ikv_array_get(tabs, index)); for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
{
const char *path = ikv_as_string(ikv_array_get(tabs, index));
open_repositories_.emplace_back(path ? path : ""); open_repositories_.emplace_back(path ? path : "");
} }
} }
@@ -75,80 +91,105 @@ void UserData::load() {
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f); sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
details_width_ = std::clamp(details_width_, 280.0f, 650.0f); details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
pull_mode_ = std::clamp(pull_mode_, 0, 3); pull_mode_ = std::clamp(pull_mode_, 0, 3);
for (float& height : sidebar_section_heights_) height = std::clamp(height, 42.0f, 500.0f); for (float &height : sidebar_section_heights_)
if (open_repositories_.empty()) active_repository_ = 0; height = std::clamp(height, 42.0f, 500.0f);
else active_repository_ = std::min(active_repository_, open_repositories_.size() - 1); if (open_repositories_.empty())
active_repository_ = 0;
else
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
return; return;
} }
// Import the original files once when upgrading an existing installation. // Import the original files once when upgrading an existing installation.
std::ifstream settings(directory_ / "settings.ini"); std::ifstream settings(directory_ / "settings.ini");
std::string key; std::string key;
while (settings >> key) { while (settings >> key)
if (key == "sidebar_width") settings >> sidebar_width_; {
else if (key == "details_width") settings >> details_width_; if (key == "sidebar_width")
else if (key == "pull_mode") settings >> pull_mode_; settings >> sidebar_width_;
else if (key.rfind("sidebar_section_", 0) == 0) { else if (key == "details_width")
settings >> details_width_;
else if (key == "pull_mode")
settings >> pull_mode_;
else if (key.rfind("sidebar_section_", 0) == 0)
{
const size_t index = static_cast<size_t>(std::stoul(key.substr(16))); const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
float height = 0.0f; float height = 0.0f;
settings >> height; settings >> height;
if (index < sidebar_section_heights_.size()) sidebar_section_heights_[index] = height; if (index < sidebar_section_heights_.size())
sidebar_section_heights_[index] = height;
} }
} }
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f); sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
details_width_ = std::clamp(details_width_, 280.0f, 650.0f); details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
pull_mode_ = std::clamp(pull_mode_, 0, 3); pull_mode_ = std::clamp(pull_mode_, 0, 3);
for (float& height : sidebar_section_heights_) height = std::clamp(height, 42.0f, 500.0f); for (float &height : sidebar_section_heights_)
height = std::clamp(height, 42.0f, 500.0f);
std::ifstream history(directory_ / "history.txt"); std::ifstream history(directory_ / "history.txt");
std::string path; std::string path;
while (history >> std::quoted(path)) { while (history >> std::quoted(path))
if (!path.empty()) recently_closed_.push_back(path); {
if (recently_closed_.size() == 12) break; if (!path.empty())
recently_closed_.push_back(path);
if (recently_closed_.size() == 12)
break;
} }
std::ifstream session(directory_ / "session.txt"); std::ifstream session(directory_ / "session.txt");
session >> active_repository_; session >> active_repository_;
while (session >> std::quoted(path)) open_repositories_.push_back(path); while (session >> std::quoted(path))
if (open_repositories_.empty()) active_repository_ = 0; open_repositories_.push_back(path);
else active_repository_ = std::min(active_repository_, open_repositories_.size() - 1); if (open_repositories_.empty())
active_repository_ = 0;
else
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
} }
void UserData::addRecentlyClosed(const std::string& path) { void UserData::addRecentlyClosed(const std::string &path)
if (path.empty()) return; {
if (path.empty())
return;
std::erase(recently_closed_, path); std::erase(recently_closed_, path);
recently_closed_.insert(recently_closed_.begin(), path); recently_closed_.insert(recently_closed_.begin(), path);
if (recently_closed_.size() > 12) recently_closed_.resize(12); if (recently_closed_.size() > 12)
recently_closed_.resize(12);
save(); save();
} }
void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository) { void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
{
open_repositories_ = std::move(paths); open_repositories_ = std::move(paths);
active_repository_ = open_repositories_.empty() active_repository_ = open_repositories_.empty()
? 0 ? 0
: std::min(active_repository, open_repositories_.size() - 1); : std::min(active_repository, open_repositories_.size() - 1);
save(); save();
} }
void UserData::save() const { void UserData::save() const
{
std::filesystem::create_directories(directory_); std::filesystem::create_directories(directory_);
ikv_node_t* root = ikv_create_object("gitree"); ikv_node_t *root = ikv_create_object("gitree");
if (!root) return; if (!root)
return;
ikv_node_t* settings = ikv_object_add_object(root, "settings"); ikv_node_t *settings = ikv_object_add_object(root, "settings");
ikv_object_set_float(settings, "sidebar_width", sidebar_width_); ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
ikv_object_set_float(settings, "details_width", details_width_); ikv_object_set_float(settings, "details_width", details_width_);
ikv_object_set_int(settings, "pull_mode", pull_mode_); ikv_object_set_int(settings, "pull_mode", pull_mode_);
ikv_node_t* heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT); ikv_node_t *heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
for (const float height : sidebar_section_heights_) ikv_array_add_float(heights, height); for (const float height : sidebar_section_heights_)
ikv_array_add_float(heights, height);
ikv_node_t* history = ikv_object_add_array(root, "recently_closed", IKV_STRING); ikv_node_t *history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
for (const auto& path : recently_closed_) ikv_array_add_string(history, path.c_str()); for (const auto &path : recently_closed_)
ikv_array_add_string(history, path.c_str());
ikv_node_t* session = ikv_object_add_object(root, "session"); ikv_node_t *session = ikv_object_add_object(root, "session");
ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_)); ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_));
ikv_node_t* tabs = ikv_object_add_array(session, "tabs", IKV_STRING); ikv_node_t *tabs = ikv_object_add_array(session, "tabs", IKV_STRING);
for (const auto& path : open_repositories_) ikv_array_add_string(tabs, path.c_str()); for (const auto &path : open_repositories_)
ikv_array_add_string(tabs, path.c_str());
ikv_write_file((directory_ / "user_data.ikv").string().c_str(), root); ikv_write_file((directory_ / "user_data.ikv").string().c_str(), root);
ikv_free(root); ikv_free(root);

View File

@@ -5,15 +5,16 @@
#include <string> #include <string>
#include <vector> #include <vector>
class UserData { class UserData
{
public: public:
UserData(); UserData();
~UserData(); ~UserData();
[[nodiscard]] const std::filesystem::path& directory() const { return directory_; } [[nodiscard]] const std::filesystem::path &directory() const { return directory_; }
[[nodiscard]] const std::string& imguiIniPath() const { return imgui_ini_path_; } [[nodiscard]] const std::string &imguiIniPath() const { return imgui_ini_path_; }
[[nodiscard]] const std::vector<std::string>& recentlyClosed() const { return recently_closed_; } [[nodiscard]] const std::vector<std::string> &recentlyClosed() const { return recently_closed_; }
[[nodiscard]] const std::vector<std::string>& openRepositories() const { return open_repositories_; } [[nodiscard]] const std::vector<std::string> &openRepositories() const { return open_repositories_; }
[[nodiscard]] size_t activeRepository() const { return active_repository_; } [[nodiscard]] size_t activeRepository() const { return active_repository_; }
[[nodiscard]] float sidebarWidth() const { return sidebar_width_; } [[nodiscard]] float sidebarWidth() const { return sidebar_width_; }
[[nodiscard]] float detailsWidth() const { return details_width_; } [[nodiscard]] float detailsWidth() const { return details_width_; }
@@ -23,8 +24,12 @@ public:
void setSidebarWidth(float width) { sidebar_width_ = width; } void setSidebarWidth(float width) { sidebar_width_ = width; }
void setDetailsWidth(float width) { details_width_ = width; } void setDetailsWidth(float width) { details_width_ = width; }
void setSidebarSectionHeight(size_t index, float height) { sidebar_section_heights_.at(index) = height; } void setSidebarSectionHeight(size_t index, float height) { sidebar_section_heights_.at(index) = height; }
void setPullMode(int mode) { pull_mode_ = mode; save(); } void setPullMode(int mode)
void addRecentlyClosed(const std::string& path); {
pull_mode_ = mode;
save();
}
void addRecentlyClosed(const std::string &path);
void setRepositorySession(std::vector<std::string> paths, size_t active_repository); void setRepositorySession(std::vector<std::string> paths, size_t active_repository);
void save() const; void save() const;

View File

@@ -2,30 +2,31 @@
struct GLFWwindow; struct GLFWwindow;
class WindowManager { class WindowManager
{
public: public:
WindowManager() = default; WindowManager() = default;
~WindowManager(); ~WindowManager();
WindowManager(const WindowManager&) = delete; WindowManager(const WindowManager &) = delete;
WindowManager& operator=(const WindowManager&) = delete; WindowManager &operator=(const WindowManager &) = delete;
bool create(const char* title, int width, int height); bool create(const char *title, int width, int height);
void destroy(); void destroy();
void pollEvents(); void pollEvents();
void swapBuffers(); void swapBuffers();
void requestClose(); void requestClose();
[[nodiscard]] bool shouldClose() const; [[nodiscard]] bool shouldClose() const;
[[nodiscard]] GLFWwindow* nativeWindow() const { return window_; } [[nodiscard]] GLFWwindow *nativeWindow() const { return window_; }
[[nodiscard]] float dpiScale() const { return dpi_scale_; } [[nodiscard]] float dpiScale() const { return dpi_scale_; }
bool consumeDpiChange(); bool consumeDpiChange();
private: private:
static void contentScaleCallback(GLFWwindow* window, float x_scale, float y_scale); static void contentScaleCallback(GLFWwindow *window, float x_scale, float y_scale);
void updateDpi(float scale); void updateDpi(float scale);
void applyNativeCaption() const; void applyNativeCaption() const;
GLFWwindow* window_ = nullptr; GLFWwindow *window_ = nullptr;
float dpi_scale_ = 1.0f; float dpi_scale_ = 1.0f;
bool dpi_changed_ = false; bool dpi_changed_ = false;
}; };

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <git2.h> #include <git2.h>
#include <map>
#include <set> #include <set>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -28,19 +29,28 @@ struct WorkingFile {
bool staged = false; bool staged = false;
}; };
struct BranchDivergence {
size_t ahead = 0;
size_t behind = 0;
};
struct CommitInfo { struct CommitInfo {
git_oid oid{}; git_oid oid{};
std::string short_id; std::string short_id;
std::string summary; std::string summary;
std::string description;
std::string author; std::string author;
std::string email; std::string email;
std::string date; std::string date;
int parents = 0; int parents = 0;
int lane = 0; int lane = 0;
int graph_color = 0;
std::vector<git_oid> parent_ids; std::vector<git_oid> parent_ids;
std::vector<RefBadge> refs; std::vector<RefBadge> refs;
std::vector<ChangedFile> changed_files; std::vector<ChangedFile> changed_files;
std::vector<ChangedFile> all_files;
bool changes_loaded = false; bool changes_loaded = false;
bool all_files_loaded = false;
}; };
struct RepositoryView { struct RepositoryView {
@@ -53,13 +63,17 @@ struct RepositoryView {
std::string branch = "detached"; std::string branch = "detached";
std::vector<std::string> local_branches; std::vector<std::string> local_branches;
std::vector<std::string> remote_branches; std::vector<std::string> remote_branches;
std::map<std::string, BranchDivergence> local_branch_divergence;
std::map<std::string, BranchDivergence> remote_branch_divergence;
std::vector<std::string> remotes; std::vector<std::string> remotes;
std::vector<std::string> worktrees; std::vector<std::string> worktrees;
std::set<std::string> worktree_branches; std::set<std::string> worktree_branches;
std::vector<std::string> submodules; std::vector<std::string> submodules;
std::map<std::string, unsigned int> submodule_statuses;
std::vector<CommitInfo> commits; std::vector<CommitInfo> commits;
std::vector<WorkingFile> working_files; std::vector<WorkingFile> working_files;
int selected_commit = 0; int selected_commit = 0;
int scroll_to_commit = -1;
RepositoryView() = default; RepositoryView() = default;
~RepositoryView() { close(); } ~RepositoryView() { close(); }

View File

@@ -5,13 +5,16 @@
#include <IconsFontAwesome6.h> #include <IconsFontAwesome6.h>
#include <imgui.h> #include <imgui.h>
#include <izo/Dialogs.hpp>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <ctime>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iomanip>
#include <initializer_list>
#include <sstream> #include <sstream>
#include <string_view>
namespace { namespace {
float scaled(float value, float scale) { return value * scale; } float scaled(float value, float scale) { return value * scale; }
@@ -38,21 +41,239 @@ void parseRange(const std::string& header, char marker, int& line) {
} }
} }
ImU32 syntaxColor(const std::string& text) { enum class SyntaxLanguage { plain, c, cpp, csharp, lua, python, javascript };
const size_t first = text.find_first_not_of(" \t"); enum class SyntaxContinuation { none, block_comment, lua_comment, python_single, python_double, template_string };
if (first == std::string::npos) return IM_COL32(210, 214, 220, 255);
const std::string_view value(text.c_str() + first, text.size() - first); struct SyntaxState {
if (value.starts_with("//") || value.starts_with("# ")) return IM_COL32(129, 184, 125, 255); SyntaxContinuation continuation = SyntaxContinuation::none;
if (value.starts_with('#')) return IM_COL32(205, 157, 222, 255); };
static constexpr const char* keywords[] = {
"class ", "struct ", "enum ", "if ", "else", "for ", "while ", "return ", ImU32 blameColor(std::string_view hash, int alpha = 255) {
"const ", "static ", "void ", "bool ", "int ", "float ", "auto ", "namespace " static constexpr ImU32 colors[] = {
IM_COL32(24, 181, 204, 255), IM_COL32(73, 123, 235, 255), IM_COL32(200, 64, 200, 255),
IM_COL32(239, 79, 89, 255), IM_COL32(255, 122, 41, 255), IM_COL32(240, 186, 46, 255),
IM_COL32(64, 186, 128, 255),
}; };
for (const char* keyword : keywords) uint32_t value = 2166136261u;
if (value.starts_with(keyword)) return IM_COL32(124, 177, 228, 255); for (const unsigned char character : hash) value = (value ^ character) * 16777619u;
if (value.find('"') != std::string_view::npos || value.find('\'') != std::string_view::npos) return (colors[value % std::size(colors)] & ~IM_COL32_A_MASK) | (static_cast<ImU32>(alpha) << IM_COL32_A_SHIFT);
return IM_COL32(226, 166, 140, 255); }
return IM_COL32(218, 221, 226, 255);
SyntaxLanguage languageForPath(const std::string& path) {
std::string extension = std::filesystem::path(path).extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) {
return static_cast<char>(std::tolower(value));
});
if (extension == ".c") return SyntaxLanguage::c;
if (extension == ".h" || extension == ".hh" || extension == ".hpp" || extension == ".hxx" ||
extension == ".cc" || extension == ".cpp" || extension == ".cxx") return SyntaxLanguage::cpp;
if (extension == ".cs") return SyntaxLanguage::csharp;
if (extension == ".lua") return SyntaxLanguage::lua;
if (extension == ".py" || extension == ".pyw") return SyntaxLanguage::python;
if (extension == ".js" || extension == ".jsx" || extension == ".mjs" || extension == ".cjs")
return SyntaxLanguage::javascript;
return SyntaxLanguage::plain;
}
bool wordIs(std::string_view word, std::initializer_list<std::string_view> values) {
return std::find(values.begin(), values.end(), word) != values.end();
}
bool isKeyword(SyntaxLanguage language, std::string_view word) {
static constexpr std::string_view common[] = {
"break", "case", "continue", "default", "do", "else", "for", "if", "return", "switch", "while"
};
if (std::find(std::begin(common), std::end(common), word) != std::end(common)) return true;
switch (language) {
case SyntaxLanguage::c:
return wordIs(word, {"const", "enum", "extern", "goto", "register", "sizeof", "static", "struct", "typedef", "union", "volatile"});
case SyntaxLanguage::cpp:
return wordIs(word, {"alignas", "auto", "class", "concept", "const", "constexpr", "consteval", "constinit", "co_await", "co_return", "co_yield", "decltype", "delete", "enum", "explicit", "export", "extern", "friend", "inline", "mutable", "namespace", "new", "noexcept", "operator", "override", "private", "protected", "public", "requires", "sizeof", "static", "struct", "template", "this", "thread_local", "throw", "try", "typedef", "typename", "union", "using", "virtual", "volatile"});
case SyntaxLanguage::csharp:
return wordIs(word, {"abstract", "as", "async", "await", "base", "class", "const", "delegate", "enum", "event", "explicit", "extern", "finally", "fixed", "foreach", "implicit", "in", "interface", "internal", "is", "lock", "namespace", "new", "operator", "out", "override", "params", "private", "protected", "public", "readonly", "record", "ref", "sealed", "sizeof", "stackalloc", "static", "struct", "this", "throw", "try", "typeof", "unchecked", "unsafe", "using", "virtual", "volatile", "where", "yield"});
case SyntaxLanguage::lua:
return wordIs(word, {"and", "elseif", "end", "false", "function", "goto", "in", "local", "nil", "not", "or", "repeat", "then", "true", "until"});
case SyntaxLanguage::python:
return wordIs(word, {"and", "as", "assert", "async", "await", "class", "def", "del", "elif", "except", "False", "finally", "from", "global", "import", "in", "is", "lambda", "None", "nonlocal", "not", "or", "pass", "raise", "True", "try", "with", "yield"});
case SyntaxLanguage::javascript:
return wordIs(word, {"async", "await", "catch", "class", "const", "debugger", "delete", "export", "extends", "false", "finally", "from", "function", "get", "import", "in", "instanceof", "let", "new", "null", "of", "set", "static", "super", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "with", "yield"});
default:
return false;
}
}
bool isTypeWord(SyntaxLanguage language, std::string_view word) {
if (language == SyntaxLanguage::python || language == SyntaxLanguage::lua || language == SyntaxLanguage::plain)
return false;
return wordIs(word, {"bool", "byte", "char", "decimal", "double", "dynamic", "float", "int", "int8_t", "int16_t", "int32_t", "int64_t", "long", "nint", "nuint", "object", "sbyte", "short", "signed", "size_t", "string", "uint", "uint8_t", "uint16_t", "uint32_t", "uint64_t", "ulong", "unsigned", "ushort", "var", "void", "wchar_t"});
}
void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
SyntaxLanguage language, SyntaxState& state) {
constexpr ImU32 normal = IM_COL32(218, 221, 226, 255);
constexpr ImU32 keyword = IM_COL32(198, 139, 230, 255);
constexpr ImU32 type = IM_COL32(91, 198, 190, 255);
constexpr ImU32 string = IM_COL32(226, 166, 140, 255);
constexpr ImU32 number = IM_COL32(181, 206, 126, 255);
constexpr ImU32 comment = IM_COL32(112, 153, 105, 255);
constexpr ImU32 function = IM_COL32(220, 199, 128, 255);
constexpr ImU32 preprocessor = IM_COL32(205, 157, 222, 255);
const auto drawSpan = [&](size_t begin, size_t end, ImU32 color) {
if (end <= begin) return;
const char* first = text.data() + begin;
const char* last = text.data() + end;
draw->AddText(position, color, first, last);
position.x += ImGui::CalcTextSize(first, last, false).x;
};
if (language == SyntaxLanguage::plain) {
drawSpan(0, text.size(), normal);
return;
}
const size_t first_non_space = text.find_first_not_of(" \t");
if ((language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) &&
first_non_space != std::string::npos && text[first_non_space] == '#') {
drawSpan(0, first_non_space, normal);
drawSpan(first_non_space, text.size(), preprocessor);
return;
}
size_t cursor = 0;
while (cursor < text.size()) {
if (state.continuation == SyntaxContinuation::block_comment) {
const size_t end = text.find("*/", cursor);
if (end == std::string::npos) { drawSpan(cursor, text.size(), comment); return; }
drawSpan(cursor, end + 2, comment);
cursor = end + 2;
state.continuation = SyntaxContinuation::none;
continue;
}
if (state.continuation == SyntaxContinuation::lua_comment) {
const size_t end = text.find("]]", cursor);
if (end == std::string::npos) { drawSpan(cursor, text.size(), comment); return; }
drawSpan(cursor, end + 2, comment);
cursor = end + 2;
state.continuation = SyntaxContinuation::none;
continue;
}
if (state.continuation == SyntaxContinuation::python_single ||
state.continuation == SyntaxContinuation::python_double) {
const std::string_view delimiter = state.continuation == SyntaxContinuation::python_single ? "'''" : "\"\"\"";
const size_t end = text.find(delimiter, cursor);
if (end == std::string::npos) { drawSpan(cursor, text.size(), string); return; }
drawSpan(cursor, end + delimiter.size(), string);
cursor = end + delimiter.size();
state.continuation = SyntaxContinuation::none;
continue;
}
if (state.continuation == SyntaxContinuation::template_string) {
const size_t end = text.find('`', cursor);
if (end == std::string::npos) { drawSpan(cursor, text.size(), string); return; }
drawSpan(cursor, end + 1, string);
cursor = end + 1;
state.continuation = SyntaxContinuation::none;
continue;
}
const bool slash_comments = language == SyntaxLanguage::c || language == SyntaxLanguage::cpp ||
language == SyntaxLanguage::csharp || language == SyntaxLanguage::javascript;
if (slash_comments && text.compare(cursor, 2, "//") == 0) {
drawSpan(cursor, text.size(), comment);
return;
}
if (slash_comments && text.compare(cursor, 2, "/*") == 0) {
const size_t end = text.find("*/", cursor + 2);
if (end == std::string::npos) {
drawSpan(cursor, text.size(), comment);
state.continuation = SyntaxContinuation::block_comment;
return;
}
drawSpan(cursor, end + 2, comment);
cursor = end + 2;
continue;
}
if (language == SyntaxLanguage::python && text[cursor] == '#') {
drawSpan(cursor, text.size(), comment);
return;
}
if (language == SyntaxLanguage::lua && text.compare(cursor, 4, "--[[") == 0) {
const size_t end = text.find("]]", cursor + 4);
if (end == std::string::npos) {
drawSpan(cursor, text.size(), comment);
state.continuation = SyntaxContinuation::lua_comment;
return;
}
drawSpan(cursor, end + 2, comment);
cursor = end + 2;
continue;
}
if (language == SyntaxLanguage::lua && text.compare(cursor, 2, "--") == 0) {
drawSpan(cursor, text.size(), comment);
return;
}
if (language == SyntaxLanguage::python &&
(text.compare(cursor, 3, "'''") == 0 || text.compare(cursor, 3, "\"\"\"") == 0)) {
const std::string_view delimiter(text.data() + cursor, 3);
const size_t end = text.find(delimiter, cursor + 3);
if (end == std::string::npos) {
drawSpan(cursor, text.size(), string);
state.continuation = delimiter[0] == '\'' ? SyntaxContinuation::python_single : SyntaxContinuation::python_double;
return;
}
drawSpan(cursor, end + 3, string);
cursor = end + 3;
continue;
}
if (text[cursor] == '\'' || text[cursor] == '"' ||
(language == SyntaxLanguage::javascript && text[cursor] == '`')) {
const char quote = text[cursor];
size_t end = cursor + 1;
bool escaped = false;
for (; end < text.size(); ++end) {
if (!escaped && text[end] == quote) { ++end; break; }
escaped = !escaped && text[end] == '\\';
if (text[end] != '\\') escaped = false;
}
const bool closed = end <= text.size() && end > cursor + 1 && text[end - 1] == quote;
drawSpan(cursor, end, string);
if (quote == '`' && !closed) state.continuation = SyntaxContinuation::template_string;
cursor = end;
continue;
}
if (std::isdigit(static_cast<unsigned char>(text[cursor]))) {
size_t end = cursor + 1;
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) ||
text[end] == '.' || text[end] == '_')) ++end;
drawSpan(cursor, end, number);
cursor = end;
continue;
}
if (std::isalpha(static_cast<unsigned char>(text[cursor])) || text[cursor] == '_') {
size_t end = cursor + 1;
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) || text[end] == '_')) ++end;
const std::string_view word(text.data() + cursor, end - cursor);
size_t next = end;
while (next < text.size() && std::isspace(static_cast<unsigned char>(text[next]))) ++next;
const ImU32 color = isKeyword(language, word) ? keyword : isTypeWord(language, word) ? type :
next < text.size() && text[next] == '(' ? function : normal;
drawSpan(cursor, end, color);
cursor = end;
continue;
}
size_t end = cursor + 1;
while (end < text.size()) {
const unsigned char value = static_cast<unsigned char>(text[end]);
const bool token_start = std::isalnum(value) || text[end] == '_' || text[end] == '\'' ||
text[end] == '"' || (language == SyntaxLanguage::javascript && text[end] == '`') ||
(slash_comments && text[end] == '/') || (language == SyntaxLanguage::python && text[end] == '#') ||
(language == SyntaxLanguage::lua && text[end] == '-');
if (token_start) break;
++end;
}
drawSpan(cursor, end, normal);
cursor = end;
}
} }
bool compactButton(const char* label, bool active = false) { bool compactButton(const char* label, bool active = false) {
@@ -66,13 +287,28 @@ bool compactButton(const char* label, bool active = false) {
void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std::string& path, void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std::string& path,
bool staged, std::string& notice) { bool staged, std::string& notice) {
path_ = path; path_ = path;
commit_id_.clear();
staged_ = staged; staged_ = staged;
mode_ = Mode::diff; mode_ = Mode::diff;
reload(repository, manager, notice); reload(repository, manager, notice);
} }
void DiffViewer::openCommit(RepositoryView& repository, GitManager& manager,
const std::string& path, const std::string& commit_id, std::string& notice) {
path_ = path;
commit_id_ = commit_id;
staged_ = false;
mode_ = Mode::diff;
reload(repository, manager, notice);
if (hunks_.empty()) {
mode_ = Mode::file;
loadSupplement(repository, manager, mode_, notice);
}
}
void DiffViewer::close() { void DiffViewer::close() {
path_.clear(); path_.clear();
commit_id_.clear();
file_header_.clear(); file_header_.clear();
hunks_.clear(); hunks_.clear();
file_lines_.clear(); file_lines_.clear();
@@ -118,16 +354,68 @@ void DiffViewer::parseDiff(const std::string& text) {
} }
} }
void DiffViewer::parseBlame(const std::string& text) {
blame_lines_.clear();
const std::vector<std::string> lines = splitLines(text);
std::string previous_hash;
for (size_t index = 0; index < lines.size();) {
std::istringstream header(lines[index++]);
BlameLine line;
int original_line = 0;
if (!(header >> line.hash >> original_line >> line.line_number)) continue;
if (line.hash.size() < 8 || !std::all_of(line.hash.begin(), line.hash.end(), [](unsigned char value) {
return std::isxdigit(value) != 0;
})) continue;
std::time_t author_time = 0;
while (index < lines.size()) {
const std::string& field = lines[index++];
if (!field.empty() && field.front() == '\t') {
line.text = field.substr(1);
break;
}
const size_t separator = field.find(' ');
const std::string_view key(field.data(), separator == std::string::npos ? field.size() : separator);
const std::string value = separator == std::string::npos ? std::string{} : field.substr(separator + 1);
if (key == "author") line.author = value;
else if (key == "author-time") {
try { author_time = static_cast<std::time_t>(std::stoll(value)); }
catch (...) { author_time = 0; }
} else if (key == "summary") line.summary = value;
}
if (author_time != 0) {
std::tm date{};
#ifdef _WIN32
localtime_s(&date, &author_time);
#else
localtime_r(&author_time, &date);
#endif
std::ostringstream formatted;
formatted << std::put_time(&date, "%Y-%m-%d");
line.date = formatted.str();
}
line.show_attribution = line.hash != previous_hash;
previous_hash = line.hash;
blame_lines_.push_back(std::move(line));
}
}
void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::string& notice) { void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::string& notice) {
std::vector<std::string> arguments{"diff", "--no-ext-diff", "--no-color", "--unified=3"}; std::vector<std::string> arguments;
if (staged_) arguments.push_back("--cached"); if (commit_id_.empty()) {
arguments = {"diff", "--no-ext-diff", "--no-color", "--unified=3"};
if (staged_) arguments.push_back("--cached");
} else {
arguments = {"show", "--first-parent", "--format=", "--no-ext-diff", "--no-color",
"--unified=3", commit_id_};
}
arguments.insert(arguments.end(), {"--", path_}); arguments.insert(arguments.end(), {"--", path_});
std::string output; std::string output;
std::string error; std::string error;
if (!manager.captureGit(repository, arguments, output, error)) notice = error; if (!manager.captureGit(repository, arguments, output, error)) notice = error;
parseDiff(output); parseDiff(output);
if (hunks_.empty()) { if (hunks_.empty() && commit_id_.empty()) {
const auto file = std::find_if(repository.working_files.begin(), repository.working_files.end(), const auto file = std::find_if(repository.working_files.begin(), repository.working_files.end(),
[this](const WorkingFile& item) { return item.path == path_; }); [this](const WorkingFile& item) { return item.path == path_; });
if (file != repository.working_files.end() && file->kind == FileChangeKind::added && !staged_) { if (file != repository.working_files.end() && file->kind == FileChangeKind::added && !staged_) {
@@ -158,7 +446,10 @@ void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager,
std::string output; std::string output;
std::string error; std::string error;
if (mode == Mode::file) { if (mode == Mode::file) {
if (staged_) { if (!commit_id_.empty()) {
if (!manager.captureGit(repository, {"show", commit_id_ + ":" + path_}, output, error))
notice = error;
} else if (staged_) {
if (!manager.captureGit(repository, {"show", ":" + path_}, output, error)) notice = error; if (!manager.captureGit(repository, {"show", ":" + path_}, output, error)) notice = error;
} else { } else {
std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary); std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary);
@@ -168,12 +459,17 @@ void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager,
} }
file_lines_ = splitLines(output); file_lines_ = splitLines(output);
} else if (mode == Mode::blame) { } else if (mode == Mode::blame) {
if (!manager.captureGit(repository, {"blame", "--date=short", "--", path_}, output, error)) notice = error; std::vector<std::string> arguments{"blame", "--line-porcelain"};
blame_lines_ = splitLines(output); if (!commit_id_.empty()) arguments.push_back(commit_id_);
arguments.insert(arguments.end(), {"--", path_});
if (!manager.captureGit(repository, arguments, output, error)) notice = error;
parseBlame(output);
} else if (mode == Mode::history) { } else if (mode == Mode::history) {
if (!manager.captureGit(repository, std::vector<std::string> arguments{
{"log", "--follow", "--date=short", "--pretty=format:%h %ad %an %s", "--", path_}, "log", "--follow", "--date=short", "--pretty=format:%h %ad %an %s"};
output, error)) notice = error; if (!commit_id_.empty()) arguments.push_back(commit_id_);
arguments.insert(arguments.end(), {"--", path_});
if (!manager.captureGit(repository, arguments, output, error)) notice = error;
history_lines_ = splitLines(output); history_lines_ = splitLines(output);
} }
} }
@@ -187,7 +483,10 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
ImGui::TextUnformatted(path_.c_str()); ImGui::TextUnformatted(path_.c_str());
ImGui::SameLine(std::max(scaled(240, scale), ImGui::GetWindowWidth() - scaled(455, scale))); ImGui::SameLine(std::max(scaled(240, scale), ImGui::GetWindowWidth() - scaled(455, scale)));
if (compactButton(staged_ ? "Staged" : "Unstaged", true)) {} const bool historical = !commit_id_.empty();
const std::string source_label = historical ? "Commit " + commit_id_.substr(0, 8)
: staged_ ? "Staged" : "Unstaged";
if (compactButton(source_label.c_str(), true)) {}
ImGui::SameLine(); ImGui::SameLine();
if (compactButton("File View", mode_ == Mode::file)) { if (compactButton("File View", mode_ == Mode::file)) {
mode_ = Mode::file; loadSupplement(repository, manager, mode_, notice); mode_ = Mode::file; loadSupplement(repository, manager, mode_, notice);
@@ -202,25 +501,23 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
if (compactButton("History", mode_ == Mode::history)) { if (compactButton("History", mode_ == Mode::history)) {
mode_ = Mode::history; loadSupplement(repository, manager, mode_, notice); mode_ = Mode::history; loadSupplement(repository, manager, mode_, notice);
} }
ImGui::SameLine(); if (!historical) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1)); ImGui::SameLine();
if (compactButton(staged_ ? "Unstage File" : "Stage File")) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1));
const bool changed = staged_ ? manager.unstageFile(repository, path_, notice) if (compactButton(staged_ ? "Unstage File" : "Stage File")) {
: manager.stageFile(repository, path_, notice); const bool changed = staged_ ? manager.unstageFile(repository, path_, notice)
if (changed) { staged_ = !staged_; reload(repository, manager, notice); } : manager.stageFile(repository, path_, notice);
if (changed) { staged_ = !staged_; reload(repository, manager, notice); }
}
ImGui::PopStyleColor();
} }
ImGui::PopStyleColor();
ImGui::SameLine(); ImGui::SameLine();
if (compactButton(ICON_FA_XMARK)) close(); if (compactButton(ICON_FA_XMARK)) close();
ImGui::Separator(); ImGui::Separator();
if (!path_.empty()) { if (!path_.empty()) {
if (compactButton(ICON_FA_PEN " Edit This File")) { ImGui::SetCursorPosX(ImGui::GetWindowWidth() - scaled(116, scale));
std::string error;
if (!izo::OpenPath(std::filesystem::path(repository.path) / path_, &error)) notice = error;
}
ImGui::SameLine(ImGui::GetWindowWidth() - scaled(116, scale));
ImGui::TextDisabled("UTF-8"); ImGui::TextDisabled("UTF-8");
if (!staged_) { if (!historical && !staged_) {
ImGui::SameLine(); ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
if (compactButton(ICON_FA_TRASH_CAN)) { if (compactButton(ICON_FA_TRASH_CAN)) {
@@ -235,7 +532,9 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
ImGuiWindowFlags_HorizontalScrollbar); ImGuiWindowFlags_HorizontalScrollbar);
const float row_height = scaled(21, scale); const float row_height = scaled(21, scale);
const float number_width = scaled(48, scale); const float number_width = scaled(48, scale);
auto draw_line = [&](const std::string& text, int old_number, int new_number, LineKind kind) { const SyntaxLanguage language = languageForPath(path_);
auto draw_line = [&](const std::string& text, int old_number, int new_number, LineKind kind,
SyntaxLanguage line_language, SyntaxState& syntax_state) {
ImGui::InvisibleButton("##line", {std::max(ImGui::GetContentRegionAvail().x, scaled(900, scale)), row_height}); ImGui::InvisibleButton("##line", {std::max(ImGui::GetContentRegionAvail().x, scaled(900, scale)), row_height});
const ImVec2 minimum = ImGui::GetItemRectMin(); const ImVec2 minimum = ImGui::GetItemRectMin();
const ImVec2 maximum = ImGui::GetItemRectMax(); const ImVec2 maximum = ImGui::GetItemRectMax();
@@ -256,10 +555,14 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
draw->AddText({minimum.x + number_width * 2 + scaled(5, scale), minimum.y + scaled(2, scale)}, draw->AddText({minimum.x + number_width * 2 + scaled(5, scale), minimum.y + scaled(2, scale)},
kind == LineKind::added ? IM_COL32(87, 190, 112, 255) : kind == LineKind::added ? IM_COL32(87, 190, 112, 255) :
kind == LineKind::removed ? IM_COL32(220, 97, 97, 255) : IM_COL32(148, 154, 164, 255), marker_text); kind == LineKind::removed ? IM_COL32(220, 97, 97, 255) : IM_COL32(148, 154, 164, 255), marker_text);
draw->AddText({minimum.x + number_width * 2 + scaled(22, scale), minimum.y + scaled(2, scale)}, drawSyntaxText(draw,
syntaxColor(text), text.c_str()); {minimum.x + number_width * 2 + scaled(22, scale), minimum.y + scaled(2, scale)},
text, line_language, syntax_state);
}; };
// Keep the first source row clear of the toolbar and aligned with a normal row inset.
ImGui::Dummy({0.0f, row_height});
if (mode_ == Mode::diff) { if (mode_ == Mode::diff) {
if (hunks_.empty()) ImGui::TextDisabled("No textual diff is available for this file."); if (hunks_.empty()) ImGui::TextDisabled("No textual diff is available for this file.");
int line_id = 0; int line_id = 0;
@@ -267,12 +570,14 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
enum class HunkAction { none, stage, discard, unstage }; enum class HunkAction { none, stage, discard, unstage };
HunkAction pending_action = HunkAction::none; HunkAction pending_action = HunkAction::none;
for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) { for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) {
SyntaxState old_syntax;
SyntaxState new_syntax;
ImGui::PushID(static_cast<int>(hunk_index)); ImGui::PushID(static_cast<int>(hunk_index));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.56f, 0.70f, 0.90f, 1)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.56f, 0.70f, 0.90f, 1));
ImGui::TextUnformatted(hunks_[hunk_index].header.c_str()); ImGui::TextUnformatted(hunks_[hunk_index].header.c_str());
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::SameLine(std::max(scaled(220, scale), ImGui::GetWindowWidth() - scaled(205, scale))); ImGui::SameLine(std::max(scaled(220, scale), ImGui::GetWindowWidth() - scaled(205, scale)));
if (!staged_) { if (!historical && !staged_) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
if (ImGui::SmallButton("Discard Hunk")) { if (ImGui::SmallButton("Discard Hunk")) {
pending_hunk = static_cast<int>(hunk_index); pending_hunk = static_cast<int>(hunk_index);
@@ -286,13 +591,15 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
pending_action = HunkAction::stage; pending_action = HunkAction::stage;
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} else if (ImGui::SmallButton("Unstage Hunk")) { } else if (!historical && ImGui::SmallButton("Unstage Hunk")) {
pending_hunk = static_cast<int>(hunk_index); pending_hunk = static_cast<int>(hunk_index);
pending_action = HunkAction::unstage; pending_action = HunkAction::unstage;
} }
for (const Line& line : hunks_[hunk_index].lines) { for (const Line& line : hunks_[hunk_index].lines) {
ImGui::PushID(line_id++); ImGui::PushID(line_id++);
draw_line(line.text, line.old_number, line.new_number, line.kind); SyntaxState& syntax_state = line.kind == LineKind::removed ? old_syntax : new_syntax;
draw_line(line.text, line.old_number, line.new_number, line.kind, language, syntax_state);
if (line.kind == LineKind::context) old_syntax = new_syntax;
ImGui::PopID(); ImGui::PopID();
} }
ImGui::PopID(); ImGui::PopID();
@@ -303,13 +610,67 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
if (manager.applyPatch(repository, hunks_[pending_hunk].patch, cached, reverse, notice)) if (manager.applyPatch(repository, hunks_[pending_hunk].patch, cached, reverse, notice))
reload(repository, manager, notice); reload(repository, manager, notice);
} }
} else if (mode_ == Mode::blame) {
SyntaxState syntax_state;
const float blame_number_width = scaled(42.0f, scale);
const float attribution_width = scaled(285.0f, scale);
for (size_t index = 0; index < blame_lines_.size(); ++index) {
const BlameLine& line = blame_lines_[index];
ImGui::PushID(static_cast<int>(index));
ImGui::InvisibleButton("##blame_line",
{std::max(ImGui::GetContentRegionAvail().x, scaled(900.0f, scale)), row_height});
const ImVec2 minimum = ImGui::GetItemRectMin();
const ImVec2 maximum = ImGui::GetItemRectMax();
ImDrawList* draw = ImGui::GetWindowDrawList();
const ImU32 color = blameColor(line.hash);
draw->AddRectFilled(minimum,
{minimum.x + blame_number_width + attribution_width, maximum.y}, blameColor(line.hash, 18));
draw->AddRectFilled(
{minimum.x + blame_number_width + attribution_width - scaled(2.0f, scale), minimum.y},
{minimum.x + blame_number_width + attribution_width, maximum.y}, color);
char line_number[16]{};
std::snprintf(line_number, sizeof(line_number), "%d", line.line_number);
draw->AddText({minimum.x + scaled(6.0f, scale), minimum.y + scaled(2.0f, scale)},
IM_COL32(151, 158, 169, 255), line_number);
if (line.show_attribution) {
const float attribution_left = minimum.x + blame_number_width + scaled(7.0f, scale);
const float attribution_right = minimum.x + blame_number_width + attribution_width - scaled(8.0f, scale);
draw->AddCircleFilled(
{attribution_left + scaled(4.0f, scale), minimum.y + row_height * 0.5f},
scaled(3.5f, scale), color);
const float date_width = ImGui::CalcTextSize(line.date.c_str()).x;
draw->PushClipRect(
{attribution_left + scaled(14.0f, scale), minimum.y},
{attribution_right - date_width - scaled(9.0f, scale), maximum.y}, true);
draw->AddText({attribution_left + scaled(14.0f, scale), minimum.y + scaled(2.0f, scale)},
IM_COL32(205, 211, 220, 255), line.summary.c_str());
draw->PopClipRect();
draw->AddText({attribution_right - date_width, minimum.y + scaled(2.0f, scale)},
IM_COL32(139, 147, 159, 255), line.date.c_str());
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) &&
ImGui::GetIO().MousePos.x < minimum.x + blame_number_width + attribution_width) {
ImGui::SetTooltip("%s\n%s\n%s %s",
line.summary.c_str(), line.author.c_str(), line.hash.substr(0, 10).c_str(), line.date.c_str());
}
}
drawSyntaxText(draw,
{minimum.x + blame_number_width + attribution_width + scaled(10.0f, scale),
minimum.y + scaled(2.0f, scale)},
line.text, language, syntax_state);
ImGui::PopID();
}
if (blame_lines_.empty()) ImGui::TextDisabled("No blame data is available for this file.");
} else { } else {
const std::vector<std::string>* lines = mode_ == Mode::file ? &file_lines_ : const std::vector<std::string>* lines = mode_ == Mode::file ? &file_lines_ : &history_lines_;
mode_ == Mode::blame ? &blame_lines_ : &history_lines_; SyntaxState syntax_state;
const SyntaxLanguage line_language = mode_ == Mode::file ? language : SyntaxLanguage::plain;
for (size_t index = 0; index < lines->size(); ++index) { for (size_t index = 0; index < lines->size(); ++index) {
ImGui::PushID(static_cast<int>(index)); ImGui::PushID(static_cast<int>(index));
draw_line((*lines)[index], mode_ == Mode::file ? static_cast<int>(index + 1) : 0, draw_line((*lines)[index], mode_ == Mode::file ? static_cast<int>(index + 1) : 0,
mode_ == Mode::file ? static_cast<int>(index + 1) : 0, LineKind::context); mode_ == Mode::file ? static_cast<int>(index + 1) : 0, LineKind::context,
line_language, syntax_state);
ImGui::PopID(); ImGui::PopID();
} }
if (lines->empty()) ImGui::TextDisabled("No data is available for this view."); if (lines->empty()) ImGui::TextDisabled("No data is available for this view.");

View File

@@ -10,6 +10,8 @@ class DiffViewer {
public: public:
void open(RepositoryView& repository, GitManager& manager, const std::string& path, void open(RepositoryView& repository, GitManager& manager, const std::string& path,
bool staged, std::string& notice); bool staged, std::string& notice);
void openCommit(RepositoryView& repository, GitManager& manager, const std::string& path,
const std::string& commit_id, std::string& notice);
void close(); void close();
bool isOpen() const { return !path_.empty(); } bool isOpen() const { return !path_.empty(); }
void draw(RepositoryView& repository, GitManager& manager, float scale, std::string& notice); void draw(RepositoryView& repository, GitManager& manager, float scale, std::string& notice);
@@ -29,17 +31,28 @@ private:
std::vector<Line> lines; std::vector<Line> lines;
std::string patch; std::string patch;
}; };
struct BlameLine {
std::string hash;
std::string author;
std::string date;
std::string summary;
std::string text;
int line_number = 0;
bool show_attribution = false;
};
std::string path_; std::string path_;
std::string commit_id_;
bool staged_ = false; bool staged_ = false;
Mode mode_ = Mode::diff; Mode mode_ = Mode::diff;
std::string file_header_; std::string file_header_;
std::vector<Hunk> hunks_; std::vector<Hunk> hunks_;
std::vector<std::string> file_lines_; std::vector<std::string> file_lines_;
std::vector<std::string> blame_lines_; std::vector<BlameLine> blame_lines_;
std::vector<std::string> history_lines_; std::vector<std::string> history_lines_;
void reload(RepositoryView& repository, GitManager& manager, std::string& notice); void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice); void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);
void parseDiff(const std::string& text); void parseDiff(const std::string& text);
void parseBlame(const std::string& text);
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,41 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
namespace {
void drawOrthogonalEdge(ImDrawList* draw, const ImVec2& child, const ImVec2& parent,
ImU32 color, float scale, int route_slot) {
const float thickness = 2.0f * scale;
const auto stroke = [&](ImU32 stroke_color, float stroke_thickness) {
draw->PathClear();
draw->PathLineTo(child);
const float horizontal = parent.x - child.x;
const float vertical = parent.y - child.y;
if (std::abs(horizontal) < 0.5f * scale || vertical <= 0.0f) {
draw->PathLineTo(parent);
draw->PathStroke(stroke_color, stroke_thickness);
return;
}
// First-parent lane changes turn on the parent commit row. Additional merge
// parents leave horizontally on the child row. Both routes terminate as
// square T-junctions instead of bending through the front or back of a node.
if (route_slot == 0) {
draw->PathLineTo({child.x, parent.y});
draw->PathLineTo(parent);
} else {
draw->PathLineTo({parent.x, child.y});
draw->PathLineTo(parent);
}
draw->PathStroke(stroke_color, stroke_thickness);
};
stroke(IM_COL32(17, 21, 27, 255), thickness + 2.4f * scale);
stroke(color, thickness);
}
} // namespace
ImU32 GraphRenderer::laneColor(int lane, int alpha) { ImU32 GraphRenderer::laneColor(int lane, int alpha) {
static constexpr ImVec4 colors[] = { static constexpr ImVec4 colors[] = {
{0.08f, 0.70f, 0.83f, 1.0f}, {0.08f, 0.70f, 0.83f, 1.0f},
@@ -25,35 +60,48 @@ ImU32 GraphRenderer::laneColor(int lane, int alpha) {
float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float scale) { float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float scale) {
int maximum_lane = 0; int maximum_lane = 0;
for (const auto& commit : commits) maximum_lane = std::max(maximum_lane, commit.lane); for (const auto& commit : commits) maximum_lane = std::max(maximum_lane, commit.lane);
return std::clamp((42.0f + maximum_lane * 18.0f) * scale, 56.0f * scale, 220.0f * scale); return std::max((46.0f + maximum_lane * 22.0f) * scale, 60.0f * scale);
} }
void GraphRenderer::drawRow(int row, const CommitInfo& commit, void GraphRenderer::drawRow(int row, const CommitInfo& commit,
const std::vector<CommitInfo>& commits, const std::vector<float>& row_heights, const std::vector<CommitInfo>& commits, const std::vector<float>& row_heights,
const std::vector<std::vector<int>>& parent_rows, AvatarCache* avatars) const { const std::vector<std::vector<int>>& parent_rows, AvatarCache* avatars,
const ImVec2* ref_connector_start) const {
ImDrawList* draw = ImGui::GetWindowDrawList(); ImDrawList* draw = ImGui::GetWindowDrawList();
const ImVec2 origin = ImGui::GetCursorScreenPos(); const ImVec2 origin = ImGui::GetCursorScreenPos();
const float row_height = row_heights[static_cast<size_t>(row)]; const float row_height = row_heights[static_cast<size_t>(row)];
const float content_height = std::max(px(1.0f), row_height - ImGui::GetStyle().CellPadding.y * 2.0f); const float content_height = std::max(px(1.0f), row_height - ImGui::GetStyle().CellPadding.y * 2.0f);
const float lane_spacing = px(18.0f); const float lane_spacing = px(22.0f);
const float x = origin.x + px(15.0f) + lane_spacing * commit.lane; const float x = origin.x + px(17.0f) + lane_spacing * commit.lane;
const float y = origin.y + content_height * 0.5f; const float y = origin.y + content_height * 0.5f;
const float cell_right = origin.x + ImGui::GetContentRegionAvail().x; const float cell_right = origin.x + ImGui::GetContentRegionAvail().x;
const float row_clip_padding = ImGui::GetStyle().CellPadding.y + px(1.0f); const float row_clip_padding = ImGui::GetStyle().CellPadding.y + px(1.0f);
// GitKraken-style lane ribbon: a quiet tint carries the branch color through // Start the lane ribbon at the profile circle's centerline. The opaque node backing
// the rest of the graph column while the far edge remains crisply identifiable. // masks its left edge, while the vertical inset keeps neighboring row tints separate.
const float ribbon_left = std::min(x, cell_right);
const float ribbon_top = origin.y + px(1.0f);
const float ribbon_bottom = origin.y + content_height - px(1.0f);
draw->AddRectFilled( draw->AddRectFilled(
{x, origin.y - row_clip_padding}, {ribbon_left, ribbon_top},
{cell_right, origin.y + content_height + row_clip_padding}, {cell_right, ribbon_bottom},
laneColor(commit.lane, 38)); laneColor(commit.graph_color, 38));
draw->AddLine( if (ref_connector_start) {
{cell_right - px(1.0f), origin.y - row_clip_padding}, const ImVec2 end{x, y};
{cell_right - px(1.0f), origin.y + content_height + row_clip_padding}, const float turn_x = end.x - px(8.0f);
laneColor(commit.lane, 220), px(2.0f)); const auto stroke_connector = [&](ImU32 color, float thickness) {
if (!commit.refs.empty()) draw->PathClear();
draw->AddLine({origin.x - ImGui::GetStyle().CellPadding.x, y}, {x, y}, draw->PathLineTo(*ref_connector_start);
laneColor(commit.lane, 150), px(1.0f)); draw->PathLineTo({turn_x, ref_connector_start->y});
draw->PathLineTo({turn_x, end.y});
draw->PathLineTo(end);
draw->PathStroke(color, thickness);
};
draw->PushClipRectFullScreen();
stroke_connector(IM_COL32(17, 21, 27, 255), px(3.8f));
stroke_connector(laneColor(commit.graph_color, 175), px(1.6f));
draw->PopClipRect();
}
std::vector<float> row_offsets(row_heights.size() + 1, 0.0f); std::vector<float> row_offsets(row_heights.size() + 1, 0.0f);
for (size_t index = 0; index < row_heights.size(); ++index) for (size_t index = 0; index < row_heights.size(); ++index)
@@ -72,49 +120,48 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
for (int child_row = 0; child_row < static_cast<int>(commits.size()); ++child_row) { for (int child_row = 0; child_row < static_cast<int>(commits.size()); ++child_row) {
if (row_heights[static_cast<size_t>(child_row)] <= 0.0f) continue; if (row_heights[static_cast<size_t>(child_row)] <= 0.0f) continue;
const CommitInfo& child = commits[static_cast<size_t>(child_row)]; const CommitInfo& child = commits[static_cast<size_t>(child_row)];
for (const int parent_row : parent_rows[static_cast<size_t>(child_row)]) { const auto& child_parents = parent_rows[static_cast<size_t>(child_row)];
for (size_t parent_index = 0; parent_index < child_parents.size(); ++parent_index) {
const int parent_row = child_parents[parent_index];
if (parent_row <= child_row || row < child_row || row > parent_row || if (parent_row <= child_row || row < child_row || row > parent_row ||
row_heights[static_cast<size_t>(parent_row)] <= 0.0f) continue; row_heights[static_cast<size_t>(parent_row)] <= 0.0f) continue;
const CommitInfo& parent = commits[static_cast<size_t>(parent_row)]; const CommitInfo& parent = commits[static_cast<size_t>(parent_row)];
const float child_x = origin.x + px(15.0f) + lane_spacing * child.lane; const float child_x = origin.x + px(17.0f) + lane_spacing * child.lane;
const float parent_x = origin.x + px(15.0f) + lane_spacing * parent.lane; const float parent_x = origin.x + px(17.0f) + lane_spacing * parent.lane;
const float child_y = center_y(child_row); const float child_y = center_y(child_row);
const float parent_y = center_y(parent_row); const float parent_y = center_y(parent_row);
// An edge belongs to the branch it leaves, including the curved transition // An edge belongs to the branch it leaves, including its square lane transition,
// into a parent lane, so its color stays consistent with the child node/ref chip. // so its color stays consistent with the child node/ref chip.
const ImU32 color = laneColor(child.lane, 225); const int edge_color = parent_index == 0 ? child.graph_color : parent.graph_color;
if (child.lane == parent.lane) { drawOrthogonalEdge(draw, {child_x, child_y}, {parent_x, parent_y},
draw->AddLine({child_x, child_y}, {parent_x, parent_y}, color, px(1.8f)); laneColor(edge_color, 235), scale_, static_cast<int>(parent_index));
continue;
}
const float bend_height = std::min(parent_y - child_y, px(24.0f));
const float curve_end_y = child_y + bend_height;
draw->AddBezierCubic(
{child_x, child_y},
{child_x, child_y + bend_height * 0.45f},
{parent_x, child_y + bend_height * 0.55f},
{parent_x, curve_end_y},
color, px(1.8f));
if (curve_end_y < parent_y)
draw->AddLine({parent_x, curve_end_y}, {parent_x, parent_y}, color, px(1.8f));
} }
} }
draw->PopClipRect(); draw->PopClipRect();
const ImU32 lane_color = laneColor(commit.lane); const ImU32 lane_color = laneColor(commit.graph_color);
const auto show_identity_tooltip = [&](float hit_radius) {
if (!ImGui::IsWindowHovered()) return;
const ImVec2 mouse = ImGui::GetIO().MousePos;
const float delta_x = mouse.x - x;
const float delta_y = mouse.y - y;
if (delta_x * delta_x + delta_y * delta_y > hit_radius * hit_radius) return;
if (commit.email.empty()) ImGui::SetTooltip("%s", commit.author.c_str());
else ImGui::SetTooltip("%s <%s>", commit.author.c_str(), commit.email.c_str());
};
if (commit.parents > 1) { if (commit.parents > 1) {
draw->AddCircleFilled({x, y}, px(4.5f), lane_color); draw->AddCircleFilled({x, y}, px(6.0f), lane_color);
draw->AddCircle({x, y}, px(6.0f), laneColor(commit.lane, 110), 0, px(1.0f)); draw->AddCircle({x, y}, px(7.5f), laneColor(commit.graph_color, 130), 0, px(1.2f));
show_identity_tooltip(px(9.0f));
ImGui::Dummy({0.0f, content_height}); ImGui::Dummy({0.0f, content_height});
return; return;
} }
const float radius = px(8.4f); const float radius = px(9.6f);
// The opaque backing masks every lane segment before the avatar is painted. // The opaque backing masks every lane segment before the avatar is painted.
draw->AddCircleFilled({x, y}, radius + px(2.2f), IM_COL32(19, 24, 31, 255)); draw->AddCircleFilled({x, y}, radius + px(1.8f), IM_COL32(19, 24, 31, 255));
draw->AddCircle({x, y}, radius + px(1.0f), lane_color, 0, px(2.0f)); draw->AddCircle({x, y}, radius + px(0.8f), lane_color, 0, px(2.0f));
draw->AddCircleFilled({x, y}, radius - px(1.0f), IM_COL32(232, 238, 242, 255)); draw->AddCircleFilled({x, y}, radius - px(1.0f), IM_COL32(232, 238, 242, 255));
const unsigned int texture = avatars ? avatars->textureFor(commit.email) : 0; const unsigned int texture = avatars ? avatars->textureFor(commit.email) : 0;
@@ -127,5 +174,6 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
const ImVec2 icon_size = ImGui::CalcTextSize(ICON_FA_USER); const ImVec2 icon_size = ImGui::CalcTextSize(ICON_FA_USER);
draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, lane_color, ICON_FA_USER); draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, lane_color, ICON_FA_USER);
} }
show_identity_tooltip(radius + px(2.0f));
ImGui::Dummy({0.0f, content_height}); ImGui::Dummy({0.0f, content_height});
} }

View File

@@ -15,7 +15,7 @@ public:
void drawRow(int row, const CommitInfo& commit, const std::vector<CommitInfo>& commits, void drawRow(int row, const CommitInfo& commit, const std::vector<CommitInfo>& commits,
const std::vector<float>& row_heights, const std::vector<std::vector<int>>& parent_rows, const std::vector<float>& row_heights, const std::vector<std::vector<int>>& parent_rows,
AvatarCache* avatars) const; AvatarCache* avatars, const ImVec2* ref_connector_start = nullptr) const;
private: private:
float px(float value) const { return value * scale_; } float px(float value) const { return value * scale_; }