Files
Gitree/src/main.cpp

893 lines
36 KiB
C++

#include <git2.h>
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <izo/dialogs.hpp>
#include <IconsFontAwesome6.h>
#include "user_data.h"
#include "window_manager.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>
namespace {
enum class RefKind { local, remote, tag };
struct RefBadge {
std::string name;
RefKind kind = RefKind::local;
bool current = false;
bool worktree = false;
bool upstream = false;
};
struct CommitInfo {
git_oid oid{};
std::string short_id;
std::string summary;
std::string author;
std::string email;
std::string date;
int parents = 0;
std::vector<RefBadge> refs;
};
struct RepositoryView {
git_repository* repo = nullptr;
std::string path;
std::string name = "New Tab";
std::string branch = "detached";
std::vector<std::string> local_branches;
std::vector<std::string> remote_branches;
std::vector<std::string> remotes;
std::vector<std::string> worktrees;
std::set<std::string> worktree_branches;
std::vector<std::string> submodules;
std::vector<CommitInfo> commits;
int selected_commit = 0;
~RepositoryView() { close(); }
void close() {
if (repo) git_repository_free(repo);
repo = nullptr;
}
};
std::vector<std::unique_ptr<RepositoryView>> g_tabs;
size_t g_active_tab = 0;
UserData* g_user_data = nullptr;
std::array<char, 1024> g_path{};
std::array<char, 256> g_filter{};
std::string g_notice;
bool g_init_popup = false;
bool g_about_popup = false;
bool g_licenses_popup = false;
float g_ui_scale = 1.0f;
float g_sidebar_width = 230.0f;
WindowManager* g_window_manager = nullptr;
float ui(float value) { return value * g_ui_scale; }
RepositoryView& repo() { return *g_tabs.at(g_active_tab); }
void create_new_tab() {
g_tabs.push_back(std::make_unique<RepositoryView>());
g_active_tab = g_tabs.size() - 1;
}
void close_tab(size_t index) {
if (index >= g_tabs.size()) return;
if (g_user_data && !g_tabs[index]->path.empty()) g_user_data->addRecentlyClosed(g_tabs[index]->path);
g_tabs.erase(g_tabs.begin() + static_cast<std::ptrdiff_t>(index));
if (g_tabs.empty()) create_new_tab();
else if (g_active_tab >= g_tabs.size()) g_active_tab = g_tabs.size() - 1;
else if (index < g_active_tab) --g_active_tab;
}
std::string last_error(const char* fallback) {
const git_error* error = git_error_last();
return error && error->message ? error->message : fallback;
}
std::string format_time(git_time_t value, int offset_minutes) {
std::time_t adjusted = static_cast<std::time_t>(value + offset_minutes * 60);
std::tm tm{};
#ifdef _WIN32
gmtime_s(&tm, &adjusted);
#else
gmtime_r(&adjusted, &tm);
#endif
char buffer[48]{};
std::strftime(buffer, sizeof(buffer), "%m/%d/%Y %I:%M %p", &tm);
return buffer;
}
void read_branches(git_branch_t type, std::vector<std::string>& output) {
git_branch_iterator* iterator = nullptr;
if (git_branch_iterator_new(&iterator, repo().repo, type) != 0) return;
git_reference* reference = nullptr;
git_branch_t found{};
while (git_branch_next(&reference, &found, iterator) == 0) {
const char* name = nullptr;
if (git_branch_name(&name, reference) == 0 && name) output.emplace_back(name);
git_reference_free(reference);
}
git_branch_iterator_free(iterator);
}
void add_badge(const git_oid* oid, RefBadge badge) {
if (!oid) return;
const auto commit = std::find_if(repo().commits.begin(), repo().commits.end(),
[oid](const CommitInfo& item) { return git_oid_equal(&item.oid, oid) != 0; });
if (commit != repo().commits.end()) commit->refs.push_back(std::move(badge));
}
void load_ref_badges() {
for (const git_branch_t type : {GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE}) {
git_branch_iterator* iterator = nullptr;
if (git_branch_iterator_new(&iterator, repo().repo, type) != 0) continue;
git_reference* reference = nullptr;
git_branch_t found{};
while (git_branch_next(&reference, &found, iterator) == 0) {
const char* name = nullptr;
if (git_branch_name(&name, reference) == 0 && name) {
RefBadge badge;
badge.name = name;
badge.kind = type == GIT_BRANCH_LOCAL ? RefKind::local : RefKind::remote;
badge.current = type == GIT_BRANCH_LOCAL && badge.name == repo().branch;
badge.worktree = type == GIT_BRANCH_LOCAL &&
(badge.current || repo().worktree_branches.contains(badge.name));
if (type == GIT_BRANCH_LOCAL) {
git_reference* upstream = nullptr;
badge.upstream = git_branch_upstream(&upstream, reference) == 0;
git_reference_free(upstream);
}
git_object* object = nullptr;
if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0) {
add_badge(git_object_id(object), std::move(badge));
git_object_free(object);
}
}
git_reference_free(reference);
}
git_branch_iterator_free(iterator);
}
git_reference_iterator* tags = nullptr;
if (git_reference_iterator_glob_new(&tags, repo().repo, "refs/tags/*") != 0) return;
git_reference* reference = nullptr;
while (git_reference_next(&reference, tags) == 0) {
git_object* object = nullptr;
if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0) {
const char* shorthand = git_reference_shorthand(reference);
add_badge(git_object_id(object), {shorthand ? shorthand : "tag", RefKind::tag});
git_object_free(object);
}
git_reference_free(reference);
}
git_reference_iterator_free(tags);
}
int submodule_callback(git_submodule* submodule, const char*, void* payload) {
auto* names = static_cast<std::vector<std::string>*>(payload);
names->emplace_back(git_submodule_name(submodule));
return 0;
}
void load_repository_data() {
repo().local_branches.clear();
repo().remote_branches.clear();
repo().remotes.clear();
repo().worktrees.clear();
repo().worktree_branches.clear();
repo().submodules.clear();
repo().commits.clear();
repo().selected_commit = 0;
const char* workdir = git_repository_workdir(repo().repo);
const char* repo_path = workdir ? workdir : git_repository_path(repo().repo);
repo().path = repo_path ? repo_path : "";
std::filesystem::path p(repo().path);
repo().name = p.filename().string();
if (repo().name.empty()) repo().name = p.parent_path().filename().string();
git_reference* head = nullptr;
if (git_repository_head(&head, repo().repo) == 0) {
const char* shorthand = git_reference_shorthand(head);
if (shorthand) repo().branch = shorthand;
git_reference_free(head);
}
read_branches(GIT_BRANCH_LOCAL, repo().local_branches);
read_branches(GIT_BRANCH_REMOTE, repo().remote_branches);
git_strarray names{};
if (git_remote_list(&names, repo().repo) == 0) {
for (size_t i = 0; i < names.count; ++i) repo().remotes.emplace_back(names.strings[i]);
git_strarray_dispose(&names);
}
if (git_worktree_list(&names, repo().repo) == 0) {
for (size_t i = 0; i < names.count; ++i) {
repo().worktrees.emplace_back(names.strings[i]);
git_worktree* worktree = nullptr;
git_repository* worktree_repo = nullptr;
git_reference* worktree_head = nullptr;
if (git_worktree_lookup(&worktree, repo().repo, names.strings[i]) == 0 &&
git_repository_open_from_worktree(&worktree_repo, worktree) == 0 &&
git_repository_head(&worktree_head, worktree_repo) == 0) {
const char* branch = git_reference_shorthand(worktree_head);
if (branch) repo().worktree_branches.emplace(branch);
}
git_reference_free(worktree_head);
git_repository_free(worktree_repo);
git_worktree_free(worktree);
}
git_strarray_dispose(&names);
}
git_submodule_foreach(repo().repo, submodule_callback, &repo().submodules);
git_revwalk* walk = nullptr;
if (git_revwalk_new(&walk, repo().repo) != 0) return;
git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
if (git_revwalk_push_head(walk) != 0) {
git_revwalk_free(walk);
return;
}
git_oid oid{};
while (repo().commits.size() < 250 && git_revwalk_next(&oid, walk) == 0) {
git_commit* commit = nullptr;
if (git_commit_lookup(&commit, repo().repo, &oid) != 0) continue;
CommitInfo item;
item.oid = oid;
char id[GIT_OID_SHA1_HEXSIZE + 1]{};
git_oid_tostr(id, sizeof(id), &oid);
item.short_id.assign(id, 8);
const char* summary = git_commit_summary(commit);
item.summary = summary ? summary : "(no commit message)";
const git_signature* author = git_commit_author(commit);
if (author) {
item.author = author->name ? author->name : "Unknown";
item.email = author->email ? author->email : "";
item.date = format_time(author->when.time, author->when.offset);
}
item.parents = static_cast<int>(git_commit_parentcount(commit));
repo().commits.push_back(std::move(item));
git_commit_free(commit);
}
git_revwalk_free(walk);
load_ref_badges();
}
bool open_repository(const char* path) {
git_repository* opened = nullptr;
if (git_repository_open_ext(&opened, path, 0, nullptr) != 0) {
g_notice = last_error("Unable to open repository");
return false;
}
repo().close();
repo().repo = opened;
load_repository_data();
g_notice = "Opened " + repo().name;
return true;
}
void pick_and_open_repository() {
izo::dialog_options options;
options.title = "Open Git repository";
if (!repo().path.empty()) options.initial_path = repo().path;
else options.initial_path = 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;
return;
}
if (result.paths.empty()) {
g_notice = "The folder picker returned no path";
return;
}
const auto utf8 = result.paths.front().u8string();
const std::string path(reinterpret_cast<const char*>(utf8.data()), utf8.size());
if (repo().repo) create_new_tab();
open_repository(path.c_str());
}
bool init_repository(const char* path) {
git_repository* created = nullptr;
if (git_repository_init(&created, path, 0) != 0) {
g_notice = last_error("Unable to initialize repository");
return false;
}
git_repository_free(created);
return open_repository(path);
}
void apply_style(float scale) {
g_ui_scale = scale;
ImGuiStyle& style = ImGui::GetStyle();
style = ImGuiStyle();
style.WindowRounding = 0.0f;
style.ChildRounding = 3.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.ScrollbarRounding = 3.0f;
style.FramePadding = {8.0f, 5.0f};
style.ItemSpacing = {8.0f, 6.0f};
style.WindowPadding = {8.0f, 8.0f};
style.Colors[ImGuiCol_Text] = ImVec4(0.89f, 0.91f, 0.94f, 1.0f);
style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.53f, 0.58f, 0.66f, 1.0f);
style.Colors[ImGuiCol_WindowBg] = ImVec4(0.100f, 0.116f, 0.145f, 1.0f);
style.Colors[ImGuiCol_ChildBg] = ImVec4(0.116f, 0.133f, 0.165f, 1.0f);
style.Colors[ImGuiCol_PopupBg] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
style.Colors[ImGuiCol_Border] = ImVec4(0.24f, 0.28f, 0.34f, 1.0f);
style.Colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.19f, 0.23f, 1.0f);
style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.24f, 0.29f, 1.0f);
style.Colors[ImGuiCol_Button] = ImVec4(0.16f, 0.19f, 0.23f, 1.0f);
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.10f, 0.44f, 0.56f, 1.0f);
style.Colors[ImGuiCol_Header] = ImVec4(0.08f, 0.40f, 0.50f, 1.0f);
style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.10f, 0.48f, 0.59f, 1.0f);
style.Colors[ImGuiCol_HeaderActive] = ImVec4(0.07f, 0.35f, 0.45f, 1.0f);
style.Colors[ImGuiCol_Tab] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
style.Colors[ImGuiCol_TabSelected] = ImVec4(0.19f, 0.22f, 0.27f, 1.0f);
style.Colors[ImGuiCol_Separator] = ImVec4(0.24f, 0.28f, 0.34f, 1.0f);
style.Colors[ImGuiCol_TableHeaderBg] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
style.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.125f, 0.143f, 0.177f, 1.0f);
style.ScaleAllSizes(scale);
}
void load_fonts(float scale) {
ImGuiIO& io = ImGui::GetIO();
io.Fonts->Clear();
ImFontConfig config;
config.OversampleH = 2;
config.OversampleV = 2;
config.PixelSnapH = false;
config.RasterizerDensity = 1.0f;
const float size = 15.0f * scale;
if (!io.Fonts->AddFontFromFileTTF(GITREE_ASSET_DIR "/InterVariable.ttf", size, &config))
io.Fonts->AddFontDefault();
static constexpr ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_16_FA, 0};
ImFontConfig icon_config;
icon_config.MergeMode = true;
icon_config.PixelSnapH = true;
icon_config.GlyphMinAdvanceX = size;
io.Fonts->AddFontFromFileTTF(
GITREE_ASSET_DIR "/fa-solid-900.ttf", size, &icon_config, icon_ranges);
apply_style(scale);
}
void section(const char* label, const std::vector<std::string>& items) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
bool open = ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 18.0f);
ImGui::TextDisabled("%d", static_cast<int>(items.size()));
ImGui::PopStyleColor();
if (open) {
for (const auto& item : items) ImGui::Selectable((" " + item).c_str());
ImGui::TreePop();
}
ImGui::Separator();
}
struct BranchNode {
std::map<std::string, BranchNode> children;
std::string full_name;
bool branch = false;
};
void add_branch_node(BranchNode& root, const std::string& branch) {
BranchNode* node = &root;
size_t start = 0;
while (start < branch.size()) {
const size_t slash = branch.find('/', start);
const std::string part = branch.substr(start, slash == std::string::npos ? std::string::npos : slash - start);
node = &node->children[part];
if (slash == std::string::npos) break;
start = slash + 1;
}
node->branch = true;
node->full_name = branch;
}
void draw_branch_nodes(const BranchNode& parent, const std::string& id_path = {}) {
for (const auto& [name, node] : parent.children) {
const std::string id = id_path + "/" + name;
if (!node.children.empty()) {
const std::string label = std::string(ICON_FA_FOLDER) + " " + name + "##" + id;
const bool open = ImGui::TreeNodeEx(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_SpanAvailWidth);
if (open) {
if (node.branch) ImGui::Selectable((std::string(ICON_FA_CODE_BRANCH) + " " + name + "##branch" + id).c_str());
draw_branch_nodes(node, id);
ImGui::TreePop();
}
} else {
ImGui::Selectable((std::string(ICON_FA_CODE_BRANCH) + " " + name + "##" + id).c_str());
}
}
}
void branch_section(const char* label, const std::vector<std::string>& branches) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
const bool open = ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ui(18.0f));
ImGui::TextDisabled("%d", static_cast<int>(branches.size()));
ImGui::PopStyleColor();
if (open) {
BranchNode root;
for (const auto& branch : branches) {
if (g_filter[0] && branch.find(g_filter.data()) == std::string::npos) continue;
add_branch_node(root, branch);
}
draw_branch_nodes(root, label);
ImGui::TreePop();
}
ImGui::Separator();
}
void draw_sidebar(float width) {
ImGui::BeginChild("sidebar", {width, -ui(28.0f)}, ImGuiChildFlags_Borders);
if (ImGui::Button(ICON_FA_LIST " List", {ui(98), ui(30)})) {}
ImGui::SameLine();
if (ImGui::Button(ICON_FA_LAYER_GROUP " Workspace", {ui(102), ui(30)})) {}
ImGui::TextDisabled("VIEWING %d", static_cast<int>(repo().local_branches.size() + repo().remote_branches.size()));
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##filter", ICON_FA_MAGNIFYING_GLASS " Search branches...", g_filter.data(), g_filter.size());
ImGui::Spacing();
branch_section(ICON_FA_HOUSE " LOCAL", repo().local_branches);
branch_section(ICON_FA_CLOUD " REMOTE", repo().remote_branches);
section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees);
section(ICON_FA_CUBES " SUBMODULES", repo().submodules);
ImGui::EndChild();
}
void draw_ref_badge(const RefBadge& badge, int index) {
std::string text;
if (badge.current) text += ICON_FA_CHECK " ";
text += badge.name;
if (badge.kind == RefKind::local) text += std::string(" ") + ICON_FA_HOUSE;
if (badge.worktree) text += std::string(" ") + ICON_FA_COMPUTER;
if (badge.kind == RefKind::remote || badge.upstream) text += std::string(" ") + ICON_FA_CLOUD;
if (badge.kind == RefKind::tag) text += std::string(" ") + ICON_FA_TAG;
const ImVec4 color = badge.kind == RefKind::tag
? ImVec4(0.42f, 0.22f, 0.62f, 1.0f)
: badge.kind == RefKind::remote
? ImVec4(0.10f, 0.31f, 0.35f, 1.0f)
: ImVec4(0.08f, 0.40f, 0.50f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Button, color);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x + 0.06f, color.y + 0.06f, color.z + 0.06f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, color);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(3.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(6.0f), ui(2.0f)});
const std::string id = text + "##ref_badge_" + std::to_string(index);
ImGui::Button(id.c_str());
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(3);
}
void draw_graph_cell(int row, int parents) {
ImDrawList* draw = ImGui::GetWindowDrawList();
ImVec2 p = ImGui::GetCursorScreenPos();
const float x = p.x + ui(28.0f) + ((parents > 1 || row % 7 == 4) ? ui(18.0f) : 0.0f);
const float y = p.y + ImGui::GetTextLineHeight() * 0.5f;
const ImU32 cyan = IM_COL32(16, 178, 214, 255);
draw->AddLine({x, p.y - ui(4.0f)}, {x, p.y + ImGui::GetTextLineHeight() + ui(5.0f)}, cyan, ui(2.0f));
if (parents > 1) draw->AddBezierCubic({x, y}, {x + ui(28), y}, {x + ui(28), y + ui(16)}, {x + ui(28), y + ui(25)}, IM_COL32(153, 55, 220, 255), ui(2.0f));
draw->AddCircleFilled({x, y}, ui(5.0f), row == 0 ? IM_COL32(240, 247, 250, 255) : cyan);
draw->AddCircle({x, y}, ui(7.0f), cyan, 0, ui(2.0f));
ImGui::Dummy({ui(78.0f), ImGui::GetTextLineHeight()});
}
void draw_commit_table() {
ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp;
if (!ImGui::BeginTable("commits", 4, flags, {-1, -1})) return;
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("BRANCH / TAG", ImGuiTableColumnFlags_WidthFixed, ui(210.0f));
ImGui::TableSetupColumn("GRAPH", ImGuiTableColumnFlags_WidthFixed, ui(92.0f));
ImGui::TableSetupColumn("COMMIT MESSAGE", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("COMMIT DATE / TIME", ImGuiTableColumnFlags_WidthFixed, ui(180.0f));
ImGui::TableHeadersRow();
for (int i = 0; i < static_cast<int>(repo().commits.size()); ++i) {
const auto& commit = repo().commits[i];
if (g_filter[0]) {
const bool message_matches = commit.summary.find(g_filter.data()) != std::string::npos;
const bool ref_matches = std::any_of(commit.refs.begin(), commit.refs.end(), [](const RefBadge& ref) {
return ref.name.find(g_filter.data()) != std::string::npos;
});
if (!message_matches && !ref_matches) continue;
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
std::string row_id = "##commit" + std::to_string(i);
if (ImGui::Selectable(row_id.c_str(), repo().selected_commit == i,
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) repo().selected_commit = i;
ImGui::SameLine(0, ui(3));
for (int ref_index = 0; ref_index < static_cast<int>(commit.refs.size()); ++ref_index) {
if (ref_index > 0) ImGui::SameLine(0, ui(4));
draw_ref_badge(commit.refs[ref_index], i * 1000 + ref_index);
}
ImGui::TableSetColumnIndex(1);
draw_graph_cell(i, commit.parents);
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(commit.summary.c_str());
ImGui::TableSetColumnIndex(3);
ImGui::TextDisabled("%s", commit.date.c_str());
}
ImGui::EndTable();
}
void draw_details() {
ImGui::BeginChild("details", {ui(360.0f), -ui(28.0f)}, ImGuiChildFlags_Borders);
if (repo().commits.empty()) {
ImGui::TextDisabled("Select a repository with commits");
ImGui::EndChild();
return;
}
const auto& commit = repo().commits[std::clamp(repo().selected_commit, 0, static_cast<int>(repo().commits.size() - 1))];
ImGui::TextDisabled("COMMIT");
ImGui::SameLine();
ImGui::Text("%s", commit.short_id.c_str());
ImGui::Separator();
ImGui::Dummy({0, 6});
ImGui::PushTextWrapPos();
ImGui::Text("%s", commit.summary.c_str());
ImGui::PopTextWrapPos();
ImGui::Dummy({0, 26});
ImGui::Separator();
ImGui::TextColored(ImVec4(0.25f, 0.80f, 0.86f, 1), "%s", commit.author.c_str());
ImGui::TextDisabled("%s", commit.email.c_str());
ImGui::TextDisabled("authored %s", commit.date.c_str());
ImGui::Dummy({0, 24});
ImGui::TextColored(ImVec4(0.95f, 0.68f, 0.22f, 1), "%d parent%s", commit.parents, commit.parents == 1 ? "" : "s");
ImGui::Separator();
ImGui::TextDisabled("Changed files and diffs will appear here");
ImGui::EndChild();
}
bool toolbar_action(const char* id, const char* icon, const char* tooltip) {
ImGui::PushID(id);
const bool clicked = ImGui::Button(icon, {ui(36), ui(36)});
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(tooltip);
ImGui::EndTooltip();
}
ImGui::PopID();
return clicked;
}
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;
}
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", url);
}
}
void draw_about_popup() {
if (g_about_popup) {
ImGui::OpenPopup("About Gitree");
g_about_popup = false;
}
if (!ImGui::BeginPopupModal("About Gitree", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.38f, 0.88f, 0.94f, 1.0f));
ImGui::SetWindowFontScale(1.35f);
ImGui::TextUnformatted("Gitree");
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
ImGui::TextDisabled("Version %s", GITREE_VERSION);
ImGui::Dummy({ui(460), ui(8)});
ImGui::TextWrapped("A fast native Git client built with C++, Dear ImGui, GLFW, and libgit2.");
ImGui::Spacing();
external_link("Gitree project page", "https://dock-it.dev/iDENTITY-Technology/Gitree");
ImGui::Separator();
if (ImGui::Button("Licenses", {ui(110), 0})) {
g_licenses_popup = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Close", {ui(90), 0})) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
void draw_licenses_popup() {
if (g_licenses_popup) {
ImGui::OpenPopup("Licenses and dependencies");
g_licenses_popup = false;
}
if (!ImGui::BeginPopupModal("Licenses and dependencies", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;
ImGui::TextUnformatted("Gitree");
ImGui::SameLine();
ImGui::TextDisabled("Creative Commons Attribution-ShareAlike 4.0");
ImGui::TextDisabled("Third-party software included with Gitree:");
ImGui::Spacing();
if (ImGui::BeginTable("dependency_licenses", 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit,
{ui(680), ui(270)})) {
ImGui::TableSetupColumn("Dependency", ImGuiTableColumnFlags_WidthFixed, ui(170));
ImGui::TableSetupColumn("License and source", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
struct Dependency { const char* name; const char* license; const char* url; };
constexpr Dependency dependencies[] = {
{"libgit2 1.9.4", "GPL-2.0 with linking exception", "https://github.com/libgit2/libgit2"},
{"Dear ImGui 1.92.8", "MIT License", "https://github.com/ocornut/imgui"},
{"GLFW 3.4", "zlib/libpng License", "https://github.com/glfw/glfw"},
{"iZo 0.1.0", "MIT License", "https://dock-it.dev/Idea-Studios/iZo"},
{"Inter", "SIL Open Font License 1.1", "https://github.com/rsms/inter"},
{"Font Awesome Free", "SIL Open Font License 1.1", "https://github.com/FortAwesome/Font-Awesome"},
};
for (const auto& dependency : dependencies) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted(dependency.name);
ImGui::TableSetColumnIndex(1);
ImGui::TextDisabled("%s", dependency.license);
external_link(dependency.url, dependency.url);
}
ImGui::EndTable();
}
ImGui::Spacing();
ImGui::TextDisabled("Complete license texts are included in the source tree with each dependency.");
if (ImGui::Button("Back", {ui(90), 0})) {
g_about_popup = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Close", {ui(90), 0})) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
void draw_popups() {
if (g_init_popup) { ImGui::OpenPopup("Create repository"); g_init_popup = false; }
if (ImGui::BeginPopupModal("Create repository", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextUnformatted("Repository folder");
ImGui::SetNextItemWidth(ui(520.0f));
const bool enter = ImGui::InputText("##path", g_path.data(), g_path.size(), ImGuiInputTextFlags_EnterReturnsTrue);
if (enter || ImGui::Button("Create", {ui(90), 0})) {
if (init_repository(g_path.data())) ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", {ui(90), 0})) ImGui::CloseCurrentPopup();
if (!g_notice.empty()) ImGui::TextColored(ImVec4(0.96f, 0.55f, 0.35f, 1), "%s", g_notice.c_str());
ImGui::EndPopup();
}
draw_about_popup();
draw_licenses_popup();
}
void draw_footer() {
ImGui::Separator();
ImGui::TextDisabled("%s", g_notice.empty() ? "Ready" : g_notice.c_str());
const char* version = "Gitree " GITREE_VERSION;
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(version).x - ui(18.0f));
ImGui::TextDisabled("%s", version);
}
void draw_new_tab() {
ImGui::BeginChild("new_tab", {-1, -ui(28.0f)}, ImGuiChildFlags_Borders);
ImGui::Dummy({0, ui(36)});
ImGui::SetCursorPosX(ui(42));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.38f, 0.88f, 0.94f, 1.0f));
ImGui::SetWindowFontScale(1.45f);
ImGui::TextUnformatted(ICON_FA_CODE_BRANCH " New Tab");
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
ImGui::SetCursorPosX(ui(42));
ImGui::TextDisabled("Open a repository or return to a recently closed tab.");
ImGui::SetCursorPosX(ui(42));
if (ImGui::Button(ICON_FA_FOLDER_OPEN " Open repository", {ui(190), ui(38)}))
pick_and_open_repository();
ImGui::Dummy({0, ui(24)});
ImGui::SetCursorPosX(ui(42));
ImGui::TextUnformatted("RECENTLY CLOSED");
ImGui::SetCursorPosX(ui(42));
ImGui::BeginChild("recently_closed", {-ui(42), ui(300)}, ImGuiChildFlags_Borders);
const auto& recent = g_user_data->recentlyClosed();
if (recent.empty()) {
ImGui::TextDisabled("No recently closed repositories yet.");
} else {
for (int i = 0; i < static_cast<int>(recent.size()); ++i) {
const std::filesystem::path path(recent[i]);
std::string name = path.filename().string();
if (name.empty()) name = path.parent_path().filename().string();
ImGui::PushID(i);
if (ImGui::Selectable((std::string(ICON_FA_CLOCK_ROTATE_LEFT) + " " + name).c_str(), false,
ImGuiSelectableFlags_AllowDoubleClick, {0, ui(42)})) {
open_repository(recent[i].c_str());
}
ImGui::SameLine(ui(260));
ImGui::TextDisabled("%s", recent[i].c_str());
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::EndChild();
}
void draw_app() {
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::Begin("Gitree", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar);
if (ImGui::BeginMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Open repository...", "Ctrl+O")) pick_and_open_repository();
if (ImGui::MenuItem("Create repository...", "Ctrl+N")) g_init_popup = true;
ImGui::Separator();
if (ImGui::MenuItem("Exit") && g_window_manager) g_window_manager->requestClose();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) { ImGui::MenuItem("Preferences", nullptr, false, false); ImGui::EndMenu(); }
if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Refresh", "F5"); ImGui::EndMenu(); }
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About Gitree")) g_about_popup = true;
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
size_t tab_to_close = g_tabs.size();
bool add_tab = false;
if (ImGui::BeginTabBar("repositories", ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable)) {
for (size_t i = 0; i < g_tabs.size(); ++i) {
bool open = true;
const std::string label = g_tabs[i]->name + "###repo_tab_" + std::to_string(i);
const ImGuiTabItemFlags flags = i == g_active_tab ? ImGuiTabItemFlags_SetSelected : ImGuiTabItemFlags_None;
if (ImGui::BeginTabItem(label.c_str(), &open, flags)) {
g_active_tab = i;
ImGui::EndTabItem();
}
if (!open) tab_to_close = i;
}
ImGui::SameLine();
if (ImGui::SmallButton(ICON_FA_PLUS "##new_tab")) add_tab = true;
ImGui::EndTabBar();
}
if (tab_to_close < g_tabs.size()) close_tab(tab_to_close);
if (add_tab) create_new_tab();
ImGui::Separator();
if (!repo().repo) {
draw_new_tab();
draw_footer();
draw_popups();
ImGui::End();
return;
}
ImGui::BeginChild("repo_toolbar", {-1, ui(56)}, ImGuiChildFlags_Borders);
ImGui::TextDisabled("repository");
ImGui::SameLine(ui(145)); ImGui::TextDisabled("branch");
ImGui::SetCursorPosY(ui(26));
ImGui::Text("%s", repo().name.c_str());
ImGui::SameLine(ui(145));
ImGui::SetNextItemWidth(ui(180));
if (ImGui::BeginCombo("##branch", repo().branch.c_str())) {
for (const auto& branch : repo().local_branches) ImGui::Selectable(branch.c_str(), branch == repo().branch);
ImGui::EndCombo();
}
constexpr int action_count = 6;
const float action_size = ui(36.0f);
const float action_spacing = ui(7.0f);
const float action_group_width = action_size * action_count + action_spacing * (action_count - 1);
const float centered_x = (ImGui::GetWindowWidth() - action_group_width) * 0.5f;
ImGui::SetCursorPos({std::max(ui(350.0f), centered_x), ui(10.0f)});
if (toolbar_action("pull", ICON_FA_DOWNLOAD, "Pull from remote")) g_notice = "Pull is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("push", ICON_FA_UPLOAD, "Push to remote")) g_notice = "Push is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("branch", ICON_FA_CODE_BRANCH, "Create branch")) g_notice = "Branch creation is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("stash", ICON_FA_BOX_ARCHIVE, "Stash changes")) g_notice = "Stash is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("pop", ICON_FA_BOX_OPEN, "Pop latest stash")) g_notice = "Stash pop is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("refresh", ICON_FA_ROTATE, "Refresh repository")) load_repository_data();
ImGui::EndChild();
draw_sidebar(ui(g_sidebar_width));
ImGui::SameLine(0, 0);
ImGui::InvisibleButton("##sidebar_splitter", {ui(6.0f), -ui(28.0f)});
if (ImGui::IsItemHovered() || ImGui::IsItemActive()) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
if (ImGui::IsItemActive()) {
g_sidebar_width = std::clamp(g_sidebar_width + ImGui::GetIO().MouseDelta.x / g_ui_scale, 180.0f, 520.0f);
}
const ImVec2 splitter_min = ImGui::GetItemRectMin();
const ImVec2 splitter_max = ImGui::GetItemRectMax();
ImGui::GetWindowDrawList()->AddLine(
{(splitter_min.x + splitter_max.x) * 0.5f, splitter_min.y},
{(splitter_min.x + splitter_max.x) * 0.5f, splitter_max.y},
ImGui::IsItemHovered() || ImGui::IsItemActive() ? IM_COL32(36, 178, 205, 255) : IM_COL32(62, 72, 88, 255),
ui(1.0f));
ImGui::SameLine(0, 0);
const float detail_width = ui(368.0f);
ImGui::BeginChild("center", {ImGui::GetContentRegionAvail().x - detail_width, -24.0f}, false);
draw_commit_table();
ImGui::EndChild();
ImGui::SameLine();
draw_details();
draw_footer();
draw_popups();
ImGui::End();
}
} // namespace
int main(int argc, char** argv) {
git_libgit2_init();
UserData user_data;
g_user_data = &user_data;
g_sidebar_width = user_data.sidebarWidth();
create_new_tab();
WindowManager window_manager;
if (!window_manager.create("Gitree", 1500, 900)) return 1;
g_window_manager = &window_manager;
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::GetIO().IniFilename = user_data.imguiIniPath().c_str();
load_fonts(window_manager.dpiScale());
ImGui_ImplGlfw_InitForOpenGL(window_manager.nativeWindow(), true);
ImGui_ImplOpenGL3_Init("#version 330");
if (argc > 1) open_repository(argv[1]);
while (!window_manager.shouldClose()) {
window_manager.pollEvents();
if (window_manager.consumeDpiChange()) load_fonts(window_manager.dpiScale());
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
draw_app();
ImGui::Render();
int width = 0, height = 0;
glfwGetFramebufferSize(window_manager.nativeWindow(), &width, &height);
glViewport(0, 0, width, height);
glClearColor(0.10f, 0.116f, 0.145f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
window_manager.swapBuffers();
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
user_data.setSidebarWidth(g_sidebar_width);
user_data.save();
ImGui::DestroyContext();
g_window_manager = nullptr;
g_tabs.clear();
g_user_data = nullptr;
git_libgit2_shutdown();
return 0;
}