#include "../subcommand/diff_subcommand.hpp"
#include
#include
#include
#include "../utils/common.hpp"
#include "../utils/git_exception.hpp"
#include "../wrapper/patch_wrapper.hpp"
#include "../wrapper/repository_wrapper.hpp"
diff_subcommand::diff_subcommand(const libgit2_object&, CLI::App& app)
{
auto* sub = app.add_subcommand("diff", "Show changes between commits, commit and working tree, etc");
sub->add_option("", m_files, "tree-ish objects to compare")->expected(0, 2);
sub->add_flag("--stat", m_stat_flag, "Generate a diffstat");
sub->add_flag("--shortstat", m_shortstat_flag, "Output only the last line of --stat");
sub->add_flag("--numstat", m_numstat_flag, "Machine-friendly --stat");
sub->add_flag("--summary", m_summary_flag, "Output a condensed summary");
sub->add_flag("--name-only", m_name_only_flag, "Show only names of changed files");
sub->add_flag("--name-status", m_name_status_flag, "Show names and status of changed files");
sub->add_flag("--raw", m_raw_flag, "Generate the diff in raw format");
sub->add_flag("--cached,--staged", m_cached_flag, "Compare staged changes to HEAD");
sub->add_flag("--no-index", m_no_index_flag, "Compare two files on filesystem");
sub->add_flag("-R", m_reverse_flag, "Swap two inputs");
sub->add_flag("-a,--text", m_text_flag, "Treat all files as text");
sub->add_flag("--ignore-space-at-eol", m_ignore_space_at_eol_flag, "Ignore changes in whitespace at EOL");
sub->add_flag("-b,--ignore-space-change", m_ignore_space_change_flag, "Ignore changes in amount of whitespace");
sub->add_flag("-w,--ignore-all-space", m_ignore_all_space_flag, "Ignore whitespace when comparing lines");
sub->add_flag("--patience", m_patience_flag, "Generate diff using patience algorithm");
sub->add_flag("--minimal", m_minimal_flag, "Spend extra time to find smallest diff");
sub->add_option("-M,--find-renames", m_rename_threshold, "Detect renames")
->expected(0, 1)
->default_val(50)
->each(
[this](const std::string&)
{
m_find_renames_flag = true;
}
);
sub->add_option("-C,--find-copies", m_copy_threshold, "Detect copies")
->expected(0, 1)
->default_val(50)
->each(
[this](const std::string&)
{
m_find_copies_flag = true;
}
);
sub->add_flag("--find-copies-harder", m_find_copies_harder_flag, "Detect copies from unmodified files");
sub->add_flag("-B,--break-rewrites", m_break_rewrites_flag, "Detect file rewrites");
sub->add_option("-U,--unified", m_context_lines, "Lines of context");
sub->add_option("--inter-hunk-context", m_interhunk_lines, "Context between hunks");
sub->add_option("--abbrev", m_abbrev, "Abbreviation length for object names")->expected(0, 1);
sub->add_flag("--color", m_colour_flag, "Show colored diff");
sub->add_flag("--no-color", m_no_colour_flag, "Turn off colored diff");
sub->callback(
[this]()
{
this->run();
}
);
}
void print_stats(
const diff_wrapper& diff,
bool use_colour,
bool stat_flag,
bool shortstat_flag,
bool numstat_flag,
bool summary_flag
)
{
git_diff_stats_format_t format;
if (stat_flag)
{
if (shortstat_flag || numstat_flag || summary_flag)
{
throw git_exception(
"Only one of --stat, --shortstat, --numstat and --summary should be provided.",
git2cpp_error_code::BAD_ARGUMENT
);
}
else
{
format = GIT_DIFF_STATS_FULL;
}
}
else if (shortstat_flag)
{
if (numstat_flag || summary_flag)
{
throw git_exception(
"Only one of --stat, --shortstat, --numstat and --summary should be provided.",
git2cpp_error_code::BAD_ARGUMENT
);
}
else
{
format = GIT_DIFF_STATS_SHORT;
}
}
else if (numstat_flag)
{
if (summary_flag)
{
throw git_exception(
"Only one of --stat, --shortstat, --numstat and --summary should be provided.",
git2cpp_error_code::BAD_ARGUMENT
);
}
else
{
format = GIT_DIFF_STATS_NUMBER;
}
}
else if (summary_flag)
{
format = GIT_DIFF_STATS_INCLUDE_SUMMARY;
}
auto stats = diff.get_stats();
auto buf = stats.to_buf(format, 80);
if (use_colour && stat_flag)
{
// Add colors to + and - characters
std::string output(buf.ptr);
bool in_parentheses = false;
for (char c : output)
{
if (c == '(')
{
in_parentheses = true;
std::cout << c;
}
else if (c == ')')
{
in_parentheses = false;
std::cout << c;
}
else if (c == '+' && !in_parentheses)
{
std::cout << termcolor::green << '+' << termcolor::reset;
}
else if (c == '-' && !in_parentheses)
{
std::cout << termcolor::red << '-' << termcolor::reset;
}
else
{
std::cout << c;
}
}
}
else
{
std::cout << buf.ptr;
}
git_buf_dispose(&buf);
}
static int colour_printer(
[[maybe_unused]] const git_diff_delta* delta,
[[maybe_unused]] const git_diff_hunk* hunk,
const git_diff_line* line,
void* payload
)
{
bool use_colour = *reinterpret_cast(payload);
// Only print origin for context/addition/deletion lines
bool print_origin = (line->origin == GIT_DIFF_LINE_CONTEXT || line->origin == GIT_DIFF_LINE_ADDITION || line->origin == GIT_DIFF_LINE_DELETION);
if (use_colour)
{
switch (line->origin)
{
case GIT_DIFF_LINE_ADDITION:
std::cout << termcolor::green;
break;
case GIT_DIFF_LINE_DELETION:
std::cout << termcolor::red;
break;
case GIT_DIFF_LINE_ADD_EOFNL:
std::cout << termcolor::green;
break;
case GIT_DIFF_LINE_DEL_EOFNL:
std::cout << termcolor::red;
break;
case GIT_DIFF_LINE_FILE_HDR:
std::cout << termcolor::bold;
break;
case GIT_DIFF_LINE_HUNK_HDR:
std::cout << termcolor::cyan;
break;
default:
break;
}
}
if (print_origin)
{
std::cout << line->origin;
}
std::cout << std::string_view(line->content, line->content_len);
if (use_colour)
{
std::cout << termcolor::reset;
}
// Print copy/rename headers ONLY after the "diff --git" line
if (line->origin == GIT_DIFF_LINE_FILE_HDR)
{
if (delta->status == GIT_DELTA_COPIED)
{
if (use_colour)
{
std::cout << termcolor::bold;
}
std::cout << "similarity index " << delta->similarity << "%\n";
std::cout << "copy from " << delta->old_file.path << "\n";
std::cout << "copy to " << delta->new_file.path << "\n";
if (use_colour)
{
std::cout << termcolor::reset;
}
}
else if (delta->status == GIT_DELTA_RENAMED)
{
if (use_colour)
{
std::cout << termcolor::bold;
}
std::cout << "similarity index " << delta->similarity << "%\n";
std::cout << "rename from " << delta->old_file.path << "\n";
std::cout << "rename to " << delta->new_file.path << "\n";
if (use_colour)
{
std::cout << termcolor::reset;
}
}
}
return 0;
}
void diff_subcommand::print_diff(diff_wrapper& diff, bool use_colour)
{
if (m_stat_flag || m_shortstat_flag || m_numstat_flag || m_summary_flag)
{
print_stats(diff, use_colour, m_stat_flag, m_shortstat_flag, m_numstat_flag, m_summary_flag);
return;
}
if (m_find_renames_flag || m_find_copies_flag || m_find_copies_harder_flag || m_break_rewrites_flag)
{
git_diff_find_options find_opts = GIT_DIFF_FIND_OPTIONS_INIT;
if (m_find_renames_flag || m_find_copies_flag)
{
find_opts.flags |= GIT_DIFF_FIND_RENAMES;
find_opts.rename_threshold = m_rename_threshold;
}
if (m_find_copies_flag)
{
find_opts.flags |= GIT_DIFF_FIND_COPIES;
find_opts.copy_threshold = m_copy_threshold;
}
if (m_find_copies_harder_flag)
{
find_opts.flags |= GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED;
}
if (m_break_rewrites_flag)
{
find_opts.flags |= GIT_DIFF_FIND_REWRITES;
}
diff.find_similar(&find_opts);
}
git_diff_format_t format = GIT_DIFF_FORMAT_PATCH;
if (m_name_only_flag)
{
format = GIT_DIFF_FORMAT_NAME_ONLY;
}
else if (m_name_status_flag)
{
format = GIT_DIFF_FORMAT_NAME_STATUS;
}
else if (m_raw_flag)
{
format = GIT_DIFF_FORMAT_RAW;
}
diff.print(format, colour_printer, &use_colour);
}
diff_wrapper compute_diff_no_index(std::vector<:string> files, git_diff_options& diffopts)
{
if (files.size() != 2)
{
throw git_exception(
"usage: git diff --no-index [] [...]",
git2cpp_error_code::BAD_ARGUMENT
);
}
git_diff_options_init(&diffopts, GIT_DIFF_OPTIONS_VERSION);
std::string file1_str = read_file(files[0]);
std::string file2_str = read_file(files[1]);
if (file1_str.empty())
{
throw git_exception("Cannot read file: " + files[0], git2cpp_error_code::GENERIC_ERROR);
}
if (file2_str.empty())
{
throw git_exception("Cannot read file: " + files[1], git2cpp_error_code::GENERIC_ERROR);
}
auto patch = patch_wrapper::patch_from_files(files[0], file1_str, files[1], file2_str, &diffopts);
auto buf = patch.to_buf();
auto diff = diff_wrapper::diff_from_buffer(buf);
git_buf_dispose(&buf);
return diff;
}
void diff_subcommand::run()
{
git_diff_options diffopts;
git_diff_options_init(&diffopts, GIT_DIFF_OPTIONS_VERSION);
bool use_colour = false;
if (m_no_colour_flag)
{
if (m_colour_flag)
{
throw git_exception(
"Only one of --color and --no-color should be provided.",
git2cpp_error_code::BAD_ARGUMENT
);
}
else
{
use_colour = false;
}
}
else if (m_colour_flag)
{
use_colour = true;
}
if (m_cached_flag && m_no_index_flag)
{
throw git_exception("--cached and --no-index are incompatible", git2cpp_error_code::BAD_ARGUMENT);
}
if (m_no_index_flag)
{
auto diff = compute_diff_no_index(m_files, diffopts);
diff_subcommand::print_diff(diff, use_colour);
}
else
{
auto directory = get_current_git_path();
auto repo = repository_wrapper::open(directory);
diffopts.context_lines = m_context_lines;
diffopts.interhunk_lines = m_interhunk_lines;
diffopts.id_abbrev = m_abbrev;
if (m_reverse_flag)
{
diffopts.flags |= GIT_DIFF_REVERSE;
}
if (m_text_flag)
{
diffopts.flags |= GIT_DIFF_FORCE_TEXT;
}
if (m_ignore_space_at_eol_flag)
{
diffopts.flags |= GIT_DIFF_IGNORE_WHITESPACE_EOL;
}
if (m_ignore_space_change_flag)
{
diffopts.flags |= GIT_DIFF_IGNORE_WHITESPACE_CHANGE;
}
if (m_ignore_all_space_flag)
{
diffopts.flags |= GIT_DIFF_IGNORE_WHITESPACE;
}
if (m_untracked_flag)
{
diffopts.flags |= GIT_DIFF_INCLUDE_UNTRACKED;
}
if (m_patience_flag)
{
diffopts.flags |= GIT_DIFF_PATIENCE;
}
if (m_minimal_flag)
{
diffopts.flags |= GIT_DIFF_MINIMAL;
}
if (m_find_copies_flag || m_find_copies_harder_flag || m_find_renames_flag)
{
diffopts.flags |= GIT_DIFF_INCLUDE_UNMODIFIED;
}
std::optional tree1;
std::optional tree2;
if (m_files.size() >= 1)
{
tree1 = repo.treeish_to_tree(m_files[0]);
}
if (m_files.size() == 2)
{
tree2 = repo.treeish_to_tree(m_files[1]);
}
auto diff = [&repo, &tree1, &tree2, &diffopts, this]()
{
if (tree1.has_value() && tree2.has_value())
{
return repo.diff_tree_to_tree(tree1.value(), tree2.value(), &diffopts);
}
else if (m_cached_flag)
{
if (!tree1)
{
tree1 = repo.treeish_to_tree("HEAD");
}
return repo.diff_tree_to_index(tree1.value(), std::nullopt, &diffopts);
}
else if (tree1)
{
return repo.diff_tree_to_workdir_with_index(tree1.value(), &diffopts);
}
else
{
return repo.diff_index_to_workdir(std::nullopt, &diffopts);
}
}();
diff_subcommand::print_diff(diff, use_colour);
}
}