Merge branch 'main' into prod
All checks were successful
Build Windows EXE / build-windows (push) Successful in 5m43s

This commit is contained in:
2026-06-18 19:54:24 -05:00
10 changed files with 107 additions and 35 deletions

BIN
assets/gitree_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View File

@@ -110,6 +110,7 @@ void GitManager::readBranches(RepositoryView& repository, git_branch_t type,
}
void GitManager::loadRefBadges(RepositoryView& repository) {
for (CommitInfo& commit : repository.commits) commit.refs.clear();
for (const git_branch_t type : {GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE}) {
git_branch_iterator* iterator = nullptr;
if (git_branch_iterator_new(&iterator, repository.repo, type) != 0) continue;
@@ -222,6 +223,9 @@ bool GitManager::loadRepositoryData(RepositoryView& repository, std::string& err
repository.local_branches.clear(); repository.remote_branches.clear(); repository.remotes.clear();
repository.worktrees.clear(); repository.worktree_branches.clear(); repository.submodules.clear();
repository.commits.clear(); repository.selected_commit = 0;
if (repository.commit_walk) git_revwalk_free(repository.commit_walk);
repository.commit_walk = nullptr;
repository.history_exhausted = false;
loadWorkingTree(repository);
const char* workdir = git_repository_workdir(repository.repo);
repository.path = workdir ? workdir : git_repository_path(repository.repo);
@@ -262,12 +266,36 @@ bool GitManager::loadRepositoryData(RepositoryView& repository, std::string& err
}
git_submodule_foreach(repository.repo, submoduleCallback, &repository.submodules);
git_revwalk* walk = nullptr;
if (git_revwalk_new(&walk, repository.repo) != 0) { error = lastError("Unable to read history"); return false; }
git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
if (git_revwalk_push_head(walk) != 0) { git_revwalk_free(walk); return true; }
if (git_revwalk_new(&repository.commit_walk, repository.repo) != 0) {
error = lastError("Unable to read history");
return false;
}
git_revwalk_sorting(repository.commit_walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
if (git_revwalk_push_head(repository.commit_walk) != 0) {
git_revwalk_free(repository.commit_walk);
repository.commit_walk = nullptr;
repository.history_exhausted = true;
return true;
}
return loadMoreCommits(repository, 500, error);
}
bool GitManager::loadMoreCommits(RepositoryView& repository, size_t page_size, std::string& error) {
if (!repository.commit_walk || repository.history_exhausted || page_size == 0) return true;
size_t loaded = 0;
git_oid oid{};
while (repository.commits.size() < 250 && git_revwalk_next(&oid, walk) == 0) {
while (loaded < page_size) {
const int walk_result = git_revwalk_next(&oid, repository.commit_walk);
if (walk_result == GIT_ITEROVER) {
repository.history_exhausted = true;
git_revwalk_free(repository.commit_walk);
repository.commit_walk = nullptr;
break;
}
if (walk_result != 0) {
error = lastError("Unable to continue reading history");
return false;
}
git_commit* commit = nullptr;
if (git_commit_lookup(&commit, repository.repo, &oid) != 0) continue;
CommitInfo item; item.oid = oid;
@@ -283,8 +311,8 @@ bool GitManager::loadRepositoryData(RepositoryView& repository, std::string& err
if (const git_oid* parent = git_commit_parent_id(commit, static_cast<unsigned int>(i))) item.parent_ids.push_back(*parent);
repository.commits.push_back(std::move(item));
git_commit_free(commit);
++loaded;
}
git_revwalk_free(walk);
computeGraphLanes(repository);
loadRefBadges(repository);
return true;

View File

@@ -12,6 +12,7 @@ public:
bool openRepository(RepositoryView& repository, const std::string& path, std::string& error);
bool initializeRepository(RepositoryView& repository, const std::string& path, std::string& error);
bool reload(RepositoryView& repository, std::string& error);
bool loadMoreCommits(RepositoryView& repository, size_t page_size, 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 pull(RepositoryView& repository, int mode, std::string& error);

View File

@@ -45,6 +45,8 @@ struct CommitInfo {
struct RepositoryView {
git_repository* repo = nullptr;
git_revwalk* commit_walk = nullptr;
bool history_exhausted = false;
std::string path;
std::string name = "New Tab";
std::string branch = "detached";
@@ -64,6 +66,8 @@ struct RepositoryView {
RepositoryView& operator=(const RepositoryView&) = delete;
void close() {
if (commit_walk) git_revwalk_free(commit_walk);
commit_walk = nullptr;
if (repo) git_repository_free(repo);
repo = nullptr;
}

View File

@@ -5,7 +5,7 @@
#include <IconsFontAwesome6.h>
#include <imgui.h>
#include <izo/dialogs.hpp>
#include <izo/Dialogs.hpp>
#include <algorithm>
#include <cctype>
@@ -216,7 +216,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
if (!path_.empty()) {
if (compactButton(ICON_FA_PEN " Edit This File")) {
std::string error;
if (!izo::open_path(std::filesystem::path(repository.path) / path_, &error)) notice = error;
if (!izo::OpenPath(std::filesystem::path(repository.path) / path_, &error)) notice = error;
}
ImGui::SameLine(ImGui::GetWindowWidth() - scaled(116, scale));
ImGui::TextDisabled("UTF-8");

View File

@@ -3,7 +3,7 @@
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <izo/dialogs.hpp>
#include <izo/Dialogs.hpp>
#include <IconsFontAwesome6.h>
#include "ui/gitree_ui.h"
#include "managers/avatar_cache.h"
@@ -25,6 +25,7 @@
#include <memory>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
namespace {
@@ -107,15 +108,15 @@ bool open_repository(const char* path) {
}
void pick_and_open_repository() {
izo::dialog_options options;
izo::DialogOptions options;
options.title = "Open Git repository";
if (!repo().path.empty()) options.initial_path = repo().path;
else options.initial_path = std::filesystem::current_path();
if (!repo().path.empty()) options.initialPath = repo().path;
else options.initialPath = std::filesystem::current_path();
const izo::dialog_result result = izo::pick_folder(options);
if (result.status == izo::dialog_status::cancelled) return;
if (result.status == izo::dialog_status::error) {
g_notice = result.error_message.empty() ? "Unable to open the folder picker" : result.error_message;
const izo::DialogResult result = izo::PickFolder(options);
if (result.status == izo::DialogStatus::Cancelled) return;
if (result.status == izo::DialogStatus::Error) {
g_notice = result.errorMessage.empty() ? "Unable to open the folder picker" : result.errorMessage;
return;
}
if (result.paths.empty()) {
@@ -355,7 +356,7 @@ void sidebar_item_context(const std::string& item, SidebarItemKind kind) {
const std::filesystem::path path = kind == SidebarItemKind::worktree
? g_git_manager->worktreePath(repo(), item, error)
: std::filesystem::path(repo().path) / item;
if (path.empty() || !izo::open_path(path, &error)) g_notice = error;
if (path.empty() || !izo::OpenPath(path, &error)) g_notice = error;
}
ImGui::Separator();
if (ImGui::MenuItem(ICON_FA_COPY " Copy name")) ImGui::SetClipboardText(item.c_str());
@@ -594,10 +595,10 @@ void draw_ref_badge(const RefBadge& badge, int index, int lane) {
const ImVec2 minimum = ImGui::GetItemRectMin();
const ImVec2 maximum = ImGui::GetItemRectMax();
ImDrawList* draw = ImGui::GetWindowDrawList();
ImU32 background = GraphRenderer::laneColor(lane, badge.current ? 205 : 135);
if (ImGui::IsItemHovered()) background = GraphRenderer::laneColor(lane, badge.current ? 235 : 175);
ImU32 background = GraphRenderer::laneColor(lane, badge.current ? 165 : 90);
if (ImGui::IsItemHovered()) background = GraphRenderer::laneColor(lane, badge.current ? 195 : 125);
draw->AddRectFilled(minimum, maximum, background);
draw->AddRect(minimum, maximum, GraphRenderer::laneColor(lane, 220));
draw->AddRect(minimum, maximum, GraphRenderer::laneColor(lane, badge.current ? 210 : 145));
float x = minimum.x + ui(6.0f);
const float text_y = minimum.y + (chip_size.y - ImGui::GetFontSize()) * 0.5f;
@@ -647,6 +648,22 @@ void draw_commit_table() {
widest_reference_row + ui(12.0f), ui(145.0f), maximum_reference_width);
const float chip_line_width = std::max(ui(40.0f), reference_width - ui(12.0f));
std::vector<float> row_heights(repo().commits.size(), 0.0f);
std::unordered_map<std::string, int> commit_rows;
commit_rows.reserve(repo().commits.size());
for (int index = 0; index < static_cast<int>(repo().commits.size()); ++index) {
char oid[GIT_OID_SHA1_HEXSIZE + 1]{};
git_oid_tostr(oid, sizeof(oid), &repo().commits[static_cast<size_t>(index)].oid);
commit_rows.emplace(oid, index);
}
std::vector<std::vector<int>> parent_rows(repo().commits.size());
for (size_t index = 0; index < repo().commits.size(); ++index) {
for (const git_oid& parent_oid : repo().commits[index].parent_ids) {
char oid[GIT_OID_SHA1_HEXSIZE + 1]{};
git_oid_tostr(oid, sizeof(oid), &parent_oid);
const auto parent = commit_rows.find(oid);
if (parent != commit_rows.end()) parent_rows[index].push_back(parent->second);
}
}
for (size_t index = 0; index < repo().commits.size(); ++index) {
const CommitInfo& commit = repo().commits[index];
if (!commit_visible(commit)) continue;
@@ -711,6 +728,7 @@ void draw_commit_table() {
const float row_height = row_heights[static_cast<size_t>(i)];
ImGui::TableNextRow(0, row_height);
ImGui::TableSetColumnIndex(0);
if (!ImGui::IsRectVisible({ui(1.0f), row_height})) continue;
const ImVec2 reference_origin = ImGui::GetCursorScreenPos();
std::string row_id = "##commit" + std::to_string(i);
if (ImGui::Selectable(row_id.c_str(), repo().selected_commit == i,
@@ -741,6 +759,12 @@ void draw_commit_table() {
float chip_x = reference_origin.x + ui(3.0f);
float chip_y = reference_origin.y + ui(0.5f);
const float chip_right = reference_origin.x + chip_line_width;
if (!commit.refs.empty()) {
const float connector_y = reference_origin.y + row_height * 0.5f;
ImGui::GetWindowDrawList()->AddLine(
{reference_origin.x, connector_y}, {chip_right + ui(6.0f), connector_y},
GraphRenderer::laneColor(commit.lane, 120), ui(1.0f));
}
for (int ref_index = 0; ref_index < static_cast<int>(commit.refs.size()); ++ref_index) {
const float badge_width = ref_badge_width(commit.refs[ref_index]);
if (chip_x > reference_origin.x + ui(3.0f) && chip_x + badge_width > chip_right) {
@@ -752,14 +776,17 @@ void draw_commit_table() {
chip_x += badge_width + ui(4.0f);
}
ImGui::TableSetColumnIndex(1);
graph.drawRow(i, commit, repo().commits, row_heights, g_avatar_cache);
graph.drawRow(i, commit, repo().commits, row_heights, parent_rows, g_avatar_cache);
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(commit.summary.c_str());
ImGui::TableSetColumnIndex(3);
ImGui::TextDisabled("%s", commit.date.c_str());
}
const bool load_more_history = !repo().history_exhausted && repo().commit_walk &&
ImGui::GetScrollMaxY() - ImGui::GetScrollY() < ImGui::GetWindowHeight() * 1.5f;
ImGui::EndTable();
ImGui::PopStyleVar();
if (load_more_history) g_git_manager->loadMoreCommits(repo(), 500, g_notice);
}
const char* change_icon(FileChangeKind kind) {
@@ -1210,7 +1237,7 @@ void external_link(const char* label, const char* url) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.31f, 0.76f, 0.92f, 1.0f));
if (ImGui::Selectable(label, false, ImGuiSelectableFlags_DontClosePopups)) {
std::string error;
if (!izo::open_path(url, &error)) g_notice = error.empty() ? "Unable to open link" : error;
if (!izo::OpenPath(url, &error)) g_notice = error.empty() ? "Unable to open link" : error;
}
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
@@ -1491,7 +1518,7 @@ void draw_new_tab() {
if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Open repository")) open_repository(recent[i].c_str());
if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Reveal in file manager")) {
std::string error;
if (!izo::reveal_in_file_manager(recent[i], &error)) g_notice = error;
if (!izo::RevealInFileManager(recent[i], &error)) g_notice = error;
}
if (ImGui::MenuItem(ICON_FA_COPY " Copy path")) ImGui::SetClipboardText(recent[i].c_str());
ImGui::EndPopup();
@@ -1571,7 +1598,7 @@ void draw_app() {
if (ImGui::BeginPopupContextItem()) {
if (!g_tabs[i]->path.empty() && ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Reveal in file manager")) {
std::string error;
if (!izo::reveal_in_file_manager(g_tabs[i]->path, &error)) g_notice = error;
if (!izo::RevealInFileManager(g_tabs[i]->path, &error)) g_notice = error;
}
if (!g_tabs[i]->path.empty() && ImGui::MenuItem(ICON_FA_COPY " Copy repository path"))
ImGui::SetClipboardText(g_tabs[i]->path.c_str());

View File

@@ -30,7 +30,7 @@ float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float
void GraphRenderer::drawRow(int row, const CommitInfo& commit,
const std::vector<CommitInfo>& commits, const std::vector<float>& row_heights,
AvatarCache* avatars) const {
const std::vector<std::vector<int>>& parent_rows, AvatarCache* avatars) const {
ImDrawList* draw = ImGui::GetWindowDrawList();
const ImVec2 origin = ImGui::GetCursorScreenPos();
const float row_height = row_heights[static_cast<size_t>(row)];
@@ -38,8 +38,23 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
const float lane_spacing = px(18.0f);
const float x = origin.x + px(15.0f) + lane_spacing * commit.lane;
const float y = origin.y + content_height * 0.5f;
const float cell_right = origin.x + ImGui::GetContentRegionAvail().x;
const float row_clip_padding = ImGui::GetStyle().CellPadding.y + px(1.0f);
// GitKraken-style lane ribbon: a quiet tint carries the branch color through
// the rest of the graph column while the far edge remains crisply identifiable.
draw->AddRectFilled(
{x, origin.y - row_clip_padding},
{cell_right, origin.y + content_height + row_clip_padding},
laneColor(commit.lane, 38));
draw->AddLine(
{cell_right - px(1.0f), origin.y - row_clip_padding},
{cell_right - px(1.0f), origin.y + content_height + row_clip_padding},
laneColor(commit.lane, 220), px(2.0f));
if (!commit.refs.empty())
draw->AddLine({origin.x - ImGui::GetStyle().CellPadding.x, y}, {x, y},
laneColor(commit.lane, 150), px(1.0f));
std::vector<float> row_offsets(row_heights.size() + 1, 0.0f);
for (size_t index = 0; index < row_heights.size(); ++index)
row_offsets[index + 1] = row_offsets[index] + row_heights[index];
@@ -57,23 +72,19 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
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;
const CommitInfo& child = commits[static_cast<size_t>(child_row)];
for (const git_oid& parent_id : child.parent_ids) {
const auto parent = std::find_if(commits.begin(), commits.end(), [&parent_id](const CommitInfo& item) {
return git_oid_equal(&item.oid, &parent_id) != 0;
});
if (parent == commits.end()) continue;
const int parent_row = static_cast<int>(std::distance(commits.begin(), parent));
for (const int parent_row : parent_rows[static_cast<size_t>(child_row)]) {
if (parent_row <= child_row || row < child_row || row > parent_row ||
row_heights[static_cast<size_t>(parent_row)] <= 0.0f) continue;
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 parent_x = origin.x + px(15.0f) + lane_spacing * parent->lane;
const float parent_x = origin.x + px(15.0f) + lane_spacing * parent.lane;
const float child_y = center_y(child_row);
const float parent_y = center_y(parent_row);
// An edge belongs to the branch it leaves, including the curved transition
// into a parent lane, so its color stays consistent with the child node/ref chip.
const ImU32 color = laneColor(child.lane, 225);
if (child.lane == parent->lane) {
if (child.lane == parent.lane) {
draw->AddLine({child_x, child_y}, {parent_x, parent_y}, color, px(1.8f));
continue;
}

View File

@@ -14,7 +14,8 @@ public:
static float requiredWidth(const std::vector<CommitInfo>& commits, float scale);
void drawRow(int row, const CommitInfo& commit, const std::vector<CommitInfo>& commits,
const std::vector<float>& row_heights, AvatarCache* avatars) const;
const std::vector<float>& row_heights, const std::vector<std::vector<int>>& parent_rows,
AvatarCache* avatars) const;
private:
float px(float value) const { return value * scale_; }

2
vendor/iZo vendored

Submodule vendor/iZo updated: 88eb3ced5b...80c6bfce90