feat(diff): add full file diff viewer
Some checks are pending
Build Windows EXE / build-windows (push) Has started running
Some checks are pending
Build Windows EXE / build-windows (push) Has started running
This commit is contained in:
@@ -60,6 +60,8 @@ add_executable(gitree WIN32
|
||||
src/ui/gitree_ui.h
|
||||
src/ui/graph_renderer.cpp
|
||||
src/ui/graph_renderer.h
|
||||
src/ui/diff_viewer.cpp
|
||||
src/ui/diff_viewer.h
|
||||
src/managers/git_manager.cpp
|
||||
src/managers/git_manager.h
|
||||
src/managers/window_manager.cpp
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "managers/git_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -309,13 +310,28 @@ bool GitManager::reload(RepositoryView& repository, std::string& error) {
|
||||
|
||||
bool GitManager::runGit(RepositoryView& repository, const std::vector<std::string>& arguments,
|
||||
const char* success_message, std::string& message, bool reload_repository) {
|
||||
if (!repository.repo) { message = "No repository is open"; return false; }
|
||||
std::string command_output;
|
||||
if (!captureGit(repository, arguments, command_output, message)) return false;
|
||||
if (reload_repository) {
|
||||
std::string reload_error;
|
||||
if (!loadRepositoryData(repository, reload_error)) {
|
||||
message = reload_error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
message = success_message;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::captureGit(RepositoryView& repository, const std::vector<std::string>& arguments,
|
||||
std::string& command_output, std::string& error) {
|
||||
if (!repository.repo) { error = "No repository is open"; return false; }
|
||||
#ifdef _WIN32
|
||||
wchar_t temporary_directory[MAX_PATH]{};
|
||||
wchar_t output_path[MAX_PATH]{};
|
||||
if (!GetTempPathW(MAX_PATH, temporary_directory) ||
|
||||
!GetTempFileNameW(temporary_directory, L"gtr", 0, output_path)) {
|
||||
message = "Unable to create Git command output file";
|
||||
error = "Unable to create Git command output file";
|
||||
return false;
|
||||
}
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
@@ -323,7 +339,7 @@ bool GitManager::runGit(RepositoryView& repository, const std::vector<std::strin
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, &security, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, nullptr);
|
||||
if (output == INVALID_HANDLE_VALUE) {
|
||||
DeleteFileW(output_path);
|
||||
message = "Unable to capture Git command output";
|
||||
error = "Unable to capture Git command output";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -351,7 +367,7 @@ bool GitManager::runGit(RepositoryView& repository, const std::vector<std::strin
|
||||
}
|
||||
FlushFileBuffers(output);
|
||||
SetFilePointer(output, 0, nullptr, FILE_BEGIN);
|
||||
std::string command_output;
|
||||
command_output.clear();
|
||||
char buffer[4096];
|
||||
DWORD bytes_read = 0;
|
||||
while (ReadFile(output, buffer, sizeof(buffer), &bytes_read, nullptr) && bytes_read)
|
||||
@@ -361,25 +377,37 @@ bool GitManager::runGit(RepositoryView& repository, const std::vector<std::strin
|
||||
while (!command_output.empty() && (command_output.back() == '\r' || command_output.back() == '\n'))
|
||||
command_output.pop_back();
|
||||
if (!started || exit_code != 0) {
|
||||
message = command_output.empty() ? "Unable to run Git command" : command_output;
|
||||
error = command_output.empty() ? "Unable to run Git command" : command_output;
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
(void)arguments;
|
||||
message = "Git commands are currently supported on Windows";
|
||||
error = "Git commands are currently supported on Windows";
|
||||
return false;
|
||||
#endif
|
||||
if (reload_repository) {
|
||||
std::string reload_error;
|
||||
if (!loadRepositoryData(repository, reload_error)) {
|
||||
message = reload_error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
message = success_message;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::applyPatch(RepositoryView& repository, const std::string& patch, bool cached,
|
||||
bool reverse, std::string& error) {
|
||||
const std::filesystem::path patch_path = std::filesystem::temp_directory_path() /
|
||||
("gitree-hunk-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".patch");
|
||||
{
|
||||
std::ofstream stream(patch_path, std::ios::binary);
|
||||
if (!stream) { error = "Unable to create temporary patch"; return false; }
|
||||
stream.write(patch.data(), static_cast<std::streamsize>(patch.size()));
|
||||
}
|
||||
std::vector<std::string> arguments{"apply", "--whitespace=nowarn"};
|
||||
if (cached) arguments.push_back("--cached");
|
||||
if (reverse) arguments.push_back("--reverse");
|
||||
arguments.push_back(patch_path.string());
|
||||
const bool applied = runGit(repository, arguments,
|
||||
reverse ? "Hunk reverted" : "Hunk staged", error);
|
||||
std::error_code remove_error;
|
||||
std::filesystem::remove(patch_path, remove_error);
|
||||
return applied;
|
||||
}
|
||||
|
||||
bool GitManager::fetch(RepositoryView& repository, const std::string& remote, std::string& error) {
|
||||
const std::vector<std::string> args = remote.empty()
|
||||
? std::vector<std::string>{"fetch", "--all", "--prune"}
|
||||
|
||||
@@ -40,6 +40,10 @@ public:
|
||||
bool updateSubmodule(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 captureGit(RepositoryView& repository, const std::vector<std::string>& arguments,
|
||||
std::string& output, std::string& error);
|
||||
bool applyPatch(RepositoryView& repository, const std::string& patch, bool cached,
|
||||
bool reverse, std::string& error);
|
||||
|
||||
private:
|
||||
static std::string lastError(const char* fallback);
|
||||
|
||||
320
src/ui/diff_viewer.cpp
Normal file
320
src/ui/diff_viewer.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#include "ui/diff_viewer.h"
|
||||
|
||||
#include "managers/git_manager.h"
|
||||
#include "models/repository.h"
|
||||
|
||||
#include <IconsFontAwesome6.h>
|
||||
#include <imgui.h>
|
||||
#include <izo/dialogs.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
float scaled(float value, float scale) { return value * scale; }
|
||||
|
||||
std::vector<std::string> splitLines(const std::string& text) {
|
||||
std::vector<std::string> lines;
|
||||
std::istringstream stream(text);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
void parseRange(const std::string& header, char marker, int& line) {
|
||||
const size_t marker_position = header.find(marker);
|
||||
if (marker_position == std::string::npos) return;
|
||||
size_t cursor = marker_position + 1;
|
||||
line = 0;
|
||||
while (cursor < header.size() && std::isdigit(static_cast<unsigned char>(header[cursor]))) {
|
||||
line = line * 10 + (header[cursor] - '0');
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
|
||||
ImU32 syntaxColor(const std::string& text) {
|
||||
const size_t first = text.find_first_not_of(" \t");
|
||||
if (first == std::string::npos) return IM_COL32(188, 192, 199, 255);
|
||||
const std::string_view value(text.c_str() + first, text.size() - first);
|
||||
if (value.starts_with("//") || value.starts_with("# ")) return IM_COL32(105, 158, 102, 255);
|
||||
if (value.starts_with('#')) return IM_COL32(187, 134, 204, 255);
|
||||
static constexpr const char* keywords[] = {
|
||||
"class ", "struct ", "enum ", "if ", "else", "for ", "while ", "return ",
|
||||
"const ", "static ", "void ", "bool ", "int ", "float ", "auto ", "namespace "
|
||||
};
|
||||
for (const char* keyword : keywords)
|
||||
if (value.starts_with(keyword)) return IM_COL32(102, 153, 204, 255);
|
||||
if (value.find('"') != std::string_view::npos || value.find('\'') != std::string_view::npos)
|
||||
return IM_COL32(206, 145, 120, 255);
|
||||
return IM_COL32(198, 201, 207, 255);
|
||||
}
|
||||
|
||||
bool compactButton(const char* label, bool active = false) {
|
||||
if (active) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f));
|
||||
const bool clicked = ImGui::SmallButton(label);
|
||||
if (active) ImGui::PopStyleColor();
|
||||
return clicked;
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std::string& path,
|
||||
bool staged, std::string& notice) {
|
||||
path_ = path;
|
||||
staged_ = staged;
|
||||
mode_ = Mode::diff;
|
||||
reload(repository, manager, notice);
|
||||
}
|
||||
|
||||
void DiffViewer::close() {
|
||||
path_.clear();
|
||||
file_header_.clear();
|
||||
hunks_.clear();
|
||||
file_lines_.clear();
|
||||
blame_lines_.clear();
|
||||
history_lines_.clear();
|
||||
}
|
||||
|
||||
void DiffViewer::parseDiff(const std::string& text) {
|
||||
file_header_.clear();
|
||||
hunks_.clear();
|
||||
Hunk* current = nullptr;
|
||||
int old_line = 0;
|
||||
int new_line = 0;
|
||||
for (const std::string& raw : splitLines(text)) {
|
||||
if (raw.starts_with("@@")) {
|
||||
hunks_.push_back({});
|
||||
current = &hunks_.back();
|
||||
current->header = raw;
|
||||
parseRange(raw, '-', old_line);
|
||||
parseRange(raw, '+', new_line);
|
||||
current->patch = file_header_ + raw + "\n";
|
||||
continue;
|
||||
}
|
||||
if (!current) {
|
||||
file_header_ += raw + "\n";
|
||||
continue;
|
||||
}
|
||||
Line line;
|
||||
line.raw = raw;
|
||||
line.text = raw.empty() ? std::string{} : raw.substr(1);
|
||||
if (!raw.empty() && raw[0] == '+') {
|
||||
line.kind = LineKind::added;
|
||||
line.new_number = new_line++;
|
||||
} else if (!raw.empty() && raw[0] == '-') {
|
||||
line.kind = LineKind::removed;
|
||||
line.old_number = old_line++;
|
||||
} else if (!raw.starts_with("\\ No newline")) {
|
||||
line.old_number = old_line++;
|
||||
line.new_number = new_line++;
|
||||
}
|
||||
current->lines.push_back(std::move(line));
|
||||
current->patch += raw + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::string& notice) {
|
||||
std::vector<std::string> arguments{"diff", "--no-ext-diff", "--no-color", "--unified=3"};
|
||||
if (staged_) arguments.push_back("--cached");
|
||||
arguments.insert(arguments.end(), {"--", path_});
|
||||
std::string output;
|
||||
std::string error;
|
||||
if (!manager.captureGit(repository, arguments, output, error)) notice = error;
|
||||
parseDiff(output);
|
||||
|
||||
if (hunks_.empty()) {
|
||||
const auto file = std::find_if(repository.working_files.begin(), repository.working_files.end(),
|
||||
[this](const WorkingFile& item) { return item.path == path_; });
|
||||
if (file != repository.working_files.end() && file->kind == FileChangeKind::added && !staged_) {
|
||||
std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary);
|
||||
std::ostringstream contents;
|
||||
contents << stream.rdbuf();
|
||||
const auto lines = splitLines(contents.str());
|
||||
file_header_ = "diff --git a/" + path_ + " b/" + path_ + "\nnew file mode 100644\n--- /dev/null\n+++ b/" + path_ + "\n";
|
||||
Hunk hunk;
|
||||
hunk.header = "@@ -0,0 +1," + std::to_string(lines.size()) + " @@";
|
||||
hunk.patch = file_header_ + hunk.header + "\n";
|
||||
int number = 1;
|
||||
for (const auto& text : lines) {
|
||||
hunk.lines.push_back({0, number++, LineKind::added, text, "+" + text});
|
||||
hunk.patch += "+" + text + "\n";
|
||||
}
|
||||
hunks_.push_back(std::move(hunk));
|
||||
}
|
||||
}
|
||||
file_lines_.clear();
|
||||
blame_lines_.clear();
|
||||
history_lines_.clear();
|
||||
if (mode_ != Mode::diff) loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
|
||||
void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode,
|
||||
std::string& notice) {
|
||||
std::string output;
|
||||
std::string error;
|
||||
if (mode == Mode::file) {
|
||||
if (staged_) {
|
||||
if (!manager.captureGit(repository, {"show", ":" + path_}, output, error)) notice = error;
|
||||
} else {
|
||||
std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary);
|
||||
std::ostringstream contents;
|
||||
contents << stream.rdbuf();
|
||||
output = contents.str();
|
||||
}
|
||||
file_lines_ = splitLines(output);
|
||||
} else if (mode == Mode::blame) {
|
||||
if (!manager.captureGit(repository, {"blame", "--date=short", "--", path_}, output, error)) notice = error;
|
||||
blame_lines_ = splitLines(output);
|
||||
} else if (mode == Mode::history) {
|
||||
if (!manager.captureGit(repository,
|
||||
{"log", "--follow", "--date=short", "--pretty=format:%h %ad %an %s", "--", path_},
|
||||
output, error)) notice = error;
|
||||
history_lines_ = splitLines(output);
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float scale, std::string& notice) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {scaled(8, scale), scaled(5, scale)});
|
||||
ImGui::BeginChild("diff_viewer", {-1, -1}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::TextColored(ImVec4(0.94f, 0.66f, 0.25f, 1), ICON_FA_PEN);
|
||||
ImGui::SameLine(0, scaled(7, scale));
|
||||
ImGui::TextUnformatted(path_.c_str());
|
||||
|
||||
ImGui::SameLine(std::max(scaled(240, scale), ImGui::GetWindowWidth() - scaled(455, scale)));
|
||||
if (compactButton(staged_ ? "Staged" : "Unstaged", true)) {}
|
||||
ImGui::SameLine();
|
||||
if (compactButton("File View", mode_ == Mode::file)) {
|
||||
mode_ = Mode::file; loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (compactButton("Diff View", mode_ == Mode::diff)) mode_ = Mode::diff;
|
||||
ImGui::SameLine(0, scaled(28, scale));
|
||||
if (compactButton("Blame", mode_ == Mode::blame)) {
|
||||
mode_ = Mode::blame; loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (compactButton("History", mode_ == Mode::history)) {
|
||||
mode_ = Mode::history; loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1));
|
||||
if (compactButton(staged_ ? "Unstage File" : "Stage File")) {
|
||||
const bool changed = staged_ ? manager.unstageFile(repository, path_, notice)
|
||||
: manager.stageFile(repository, path_, notice);
|
||||
if (changed) { staged_ = !staged_; reload(repository, manager, notice); }
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
if (compactButton(ICON_FA_XMARK)) close();
|
||||
ImGui::Separator();
|
||||
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;
|
||||
}
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - scaled(116, scale));
|
||||
ImGui::TextDisabled("UTF-8");
|
||||
if (!staged_) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
|
||||
if (compactButton(ICON_FA_TRASH_CAN)) {
|
||||
if (manager.discardFile(repository, path_, notice)) close();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImGui::BeginChild("diff_content", {-1, -1}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_HorizontalScrollbar);
|
||||
const float row_height = scaled(21, scale);
|
||||
const float number_width = scaled(48, scale);
|
||||
auto draw_line = [&](const std::string& text, int old_number, int new_number, LineKind kind) {
|
||||
ImGui::InvisibleButton("##line", {std::max(ImGui::GetContentRegionAvail().x, scaled(900, scale)), row_height});
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
const ImVec2 maximum = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
if (kind == LineKind::added) draw->AddRectFilled(minimum, maximum, IM_COL32(31, 65, 43, 225));
|
||||
else if (kind == LineKind::removed) draw->AddRectFilled(minimum, maximum, IM_COL32(70, 38, 40, 225));
|
||||
draw->AddRectFilled(minimum, {minimum.x + number_width * 2, maximum.y}, IM_COL32(31, 34, 40, 255));
|
||||
char old_buffer[16]{};
|
||||
char new_buffer[16]{};
|
||||
if (old_number) std::snprintf(old_buffer, sizeof(old_buffer), "%d", old_number);
|
||||
if (new_number) std::snprintf(new_buffer, sizeof(new_buffer), "%d", new_number);
|
||||
draw->AddText({minimum.x + scaled(5, scale), minimum.y + scaled(2, scale)},
|
||||
IM_COL32(123, 128, 138, 255), old_buffer);
|
||||
draw->AddText({minimum.x + number_width + scaled(5, scale), minimum.y + scaled(2, scale)},
|
||||
IM_COL32(123, 128, 138, 255), new_buffer);
|
||||
const char marker = kind == LineKind::added ? '+' : kind == LineKind::removed ? '-' : ' ';
|
||||
char marker_text[2]{marker, 0};
|
||||
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::removed ? IM_COL32(220, 97, 97, 255) : IM_COL32(110, 115, 125, 255), marker_text);
|
||||
draw->AddText({minimum.x + number_width * 2 + scaled(22, scale), minimum.y + scaled(2, scale)},
|
||||
syntaxColor(text), text.c_str());
|
||||
};
|
||||
|
||||
if (mode_ == Mode::diff) {
|
||||
if (hunks_.empty()) ImGui::TextDisabled("No textual diff is available for this file.");
|
||||
int line_id = 0;
|
||||
int pending_hunk = -1;
|
||||
enum class HunkAction { none, stage, discard, unstage };
|
||||
HunkAction pending_action = HunkAction::none;
|
||||
for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) {
|
||||
ImGui::PushID(static_cast<int>(hunk_index));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.56f, 0.70f, 0.90f, 1));
|
||||
ImGui::TextUnformatted(hunks_[hunk_index].header.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine(std::max(scaled(220, scale), ImGui::GetWindowWidth() - scaled(205, scale)));
|
||||
if (!staged_) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
|
||||
if (ImGui::SmallButton("Discard Hunk")) {
|
||||
pending_hunk = static_cast<int>(hunk_index);
|
||||
pending_action = HunkAction::discard;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1));
|
||||
if (ImGui::SmallButton("Stage Hunk")) {
|
||||
pending_hunk = static_cast<int>(hunk_index);
|
||||
pending_action = HunkAction::stage;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
} else if (ImGui::SmallButton("Unstage Hunk")) {
|
||||
pending_hunk = static_cast<int>(hunk_index);
|
||||
pending_action = HunkAction::unstage;
|
||||
}
|
||||
for (const Line& line : hunks_[hunk_index].lines) {
|
||||
ImGui::PushID(line_id++);
|
||||
draw_line(line.text, line.old_number, line.new_number, line.kind);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (pending_hunk >= 0 && pending_hunk < static_cast<int>(hunks_.size())) {
|
||||
const bool cached = pending_action != HunkAction::discard;
|
||||
const bool reverse = pending_action != HunkAction::stage;
|
||||
if (manager.applyPatch(repository, hunks_[pending_hunk].patch, cached, reverse, notice))
|
||||
reload(repository, manager, notice);
|
||||
}
|
||||
} else {
|
||||
const std::vector<std::string>* lines = mode_ == Mode::file ? &file_lines_ :
|
||||
mode_ == Mode::blame ? &blame_lines_ : &history_lines_;
|
||||
for (size_t index = 0; index < lines->size(); ++index) {
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
draw_line((*lines)[index], mode_ == Mode::file ? static_cast<int>(index + 1) : 0,
|
||||
mode_ == Mode::file ? static_cast<int>(index + 1) : 0, LineKind::context);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (lines->empty()) ImGui::TextDisabled("No data is available for this view.");
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
45
src/ui/diff_viewer.h
Normal file
45
src/ui/diff_viewer.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GitManager;
|
||||
struct RepositoryView;
|
||||
|
||||
class DiffViewer {
|
||||
public:
|
||||
void open(RepositoryView& repository, GitManager& manager, const std::string& path,
|
||||
bool staged, std::string& notice);
|
||||
void close();
|
||||
bool isOpen() const { return !path_.empty(); }
|
||||
void draw(RepositoryView& repository, GitManager& manager, float scale, std::string& notice);
|
||||
|
||||
private:
|
||||
enum class Mode { diff, file, blame, history };
|
||||
enum class LineKind { context, added, removed };
|
||||
struct Line {
|
||||
int old_number = 0;
|
||||
int new_number = 0;
|
||||
LineKind kind = LineKind::context;
|
||||
std::string text;
|
||||
std::string raw;
|
||||
};
|
||||
struct Hunk {
|
||||
std::string header;
|
||||
std::vector<Line> lines;
|
||||
std::string patch;
|
||||
};
|
||||
|
||||
std::string path_;
|
||||
bool staged_ = false;
|
||||
Mode mode_ = Mode::diff;
|
||||
std::string file_header_;
|
||||
std::vector<Hunk> hunks_;
|
||||
std::vector<std::string> file_lines_;
|
||||
std::vector<std::string> blame_lines_;
|
||||
std::vector<std::string> history_lines_;
|
||||
|
||||
void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
|
||||
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);
|
||||
void parseDiff(const std::string& text);
|
||||
};
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "managers/user_data.h"
|
||||
#include "managers/window_manager.h"
|
||||
#include "models/repository.h"
|
||||
#include "ui/diff_viewer.h"
|
||||
#include "ui/graph_renderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -60,6 +61,7 @@ std::array<char, 1024> g_commit_description{};
|
||||
float g_ui_scale = 1.0f;
|
||||
float g_sidebar_width = 230.0f;
|
||||
float g_details_width = 368.0f;
|
||||
DiffViewer g_diff_viewer;
|
||||
WindowManager* g_window_manager = nullptr;
|
||||
GitManager* g_git_manager = nullptr;
|
||||
ImFont* g_outline_icon_font = nullptr;
|
||||
@@ -779,6 +781,9 @@ void draw_file_row(const std::string& path, FileChangeKind kind, int id,
|
||||
bool working_file = false, bool staged = false, const std::string& action_path = {}) {
|
||||
ImGui::PushID(id);
|
||||
ImGui::InvisibleButton("##file", {-1, ui(25.0f)});
|
||||
const std::string& git_path = action_path.empty() ? path : action_path;
|
||||
if (working_file && ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||
g_diff_viewer.open(repo(), *g_git_manager, git_path, staged, g_notice);
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
const ImVec2 maximum = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
@@ -786,7 +791,6 @@ void draw_file_row(const std::string& path, FileChangeKind kind, int id,
|
||||
const float y = minimum.y + (maximum.y - minimum.y - ImGui::GetFontSize()) * 0.5f;
|
||||
draw->AddText({minimum.x + ui(4.0f), y}, ImGui::ColorConvertFloat4ToU32(change_color(kind)), change_icon(kind));
|
||||
draw->AddText({minimum.x + ui(20.0f), y}, IM_COL32(174, 179, 187, 255), path.c_str());
|
||||
const std::string& git_path = action_path.empty() ? path : action_path;
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (working_file && !staged && ImGui::MenuItem(ICON_FA_CIRCLE_PLUS " Stage file"))
|
||||
g_git_manager->stageFile(repo(), git_path, g_notice);
|
||||
@@ -1725,7 +1729,8 @@ void draw_app() {
|
||||
g_details_width = std::clamp(g_details_width, std::min(280.0f, details_maximum), details_maximum);
|
||||
const float detail_width = ui(g_details_width);
|
||||
ImGui::BeginChild("center", {content_width - detail_width - ui(6.0f), -ui(28.0f)}, false);
|
||||
draw_commit_table();
|
||||
if (g_diff_viewer.isOpen()) g_diff_viewer.draw(repo(), *g_git_manager, g_ui_scale, g_notice);
|
||||
else draw_commit_table();
|
||||
ImGui::EndChild();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::InvisibleButton("##details_splitter", {ui(6.0f), -ui(28.0f)});
|
||||
|
||||
Reference in New Issue
Block a user