|
| 1 | +#include <git2.h> |
| 2 | +#include <git2/buffer.h> |
| 3 | +#include <git2/diff.h> |
| 4 | +#include <optional> |
| 5 | +#include <termcolor/termcolor.hpp> |
| 6 | + |
| 7 | +#include "../utils/common.hpp" |
| 8 | +#include "../subcommand/diff_subcommand.hpp" |
| 9 | +#include "../wrapper/patch_wrapper.hpp" |
| 10 | +#include "../wrapper/repository_wrapper.hpp" |
| 11 | + |
| 12 | +diff_subcommand::diff_subcommand(const libgit2_object&, CLI::App& app) |
| 13 | +{ |
| 14 | + auto* sub = app.add_subcommand("diff", "Show changes between commits, commit and working tree, etc"); |
| 15 | + |
| 16 | + sub->add_option("<files>", m_files, "tree-ish objects to compare"); |
| 17 | + // sub->add_option("treeish", m_treeish1, "First tree-ish object to compare"); |
| 18 | + // sub->add_option("treeish2", m_treeish2, "Second tree-ish object to compare"); |
| 19 | + |
| 20 | + sub->add_flag("--stat", m_stat_flag, "Generate a diffstat"); |
| 21 | + sub->add_flag("--shortstat", m_shortstat_flag, "Output only the last line of --stat"); |
| 22 | + sub->add_flag("--numstat", m_numstat_flag, "Machine-friendly --stat"); |
| 23 | + sub->add_flag("--summary", m_summary_flag, "Output a condensed summary"); |
| 24 | + sub->add_flag("--name-only", m_name_only_flag, "Show only names of changed files"); |
| 25 | + sub->add_flag("--name-status", m_name_status_flag, "Show names and status of changed files"); |
| 26 | + sub->add_flag("--raw", m_raw_flag, "Generate the diff in raw format"); |
| 27 | + |
| 28 | + sub->add_flag("--cached,--staged", m_cached_flag, "Compare staged changes to HEAD"); |
| 29 | + sub->add_flag("--no-index", m_no_index_flag, "Compare two files on filesystem"); |
| 30 | + |
| 31 | + sub->add_flag("-R", m_reverse_flag, "Swap two inputs"); |
| 32 | + sub->add_flag("-a,--text", m_text_flag, "Treat all files as text"); |
| 33 | + sub->add_flag("--ignore-space-at-eol", m_ignore_space_at_eol_flag, "Ignore changes in whitespace at EOL"); |
| 34 | + sub->add_flag("-b,--ignore-space-change", m_ignore_space_change_flag, "Ignore changes in amount of whitespace"); |
| 35 | + sub->add_flag("-w,--ignore-all-space", m_ignore_all_space_flag, "Ignore whitespace when comparing lines"); |
| 36 | + sub->add_flag("--patience", m_patience_flag, "Generate diff using patience algorithm"); |
| 37 | + sub->add_flag("--minimal", m_minimal_flag, "Spend extra time to find smallest diff"); |
| 38 | + |
| 39 | + sub->add_option("-M,--find-renames", m_rename_threshold, "Detect renames") |
| 40 | + ->expected(0,1) |
| 41 | + ->each([this](const std::string&) { m_find_renames_flag = true; }); |
| 42 | + sub->add_option("-C,--find-copies", m_copy_threshold, "Detect copies") |
| 43 | + ->expected(0,1) |
| 44 | + ->each([this](const std::string&) { m_find_copies_flag = true; }); |
| 45 | + sub->add_flag("--find-copies-harder", m_find_copies_harder_flag, "Detect copies from unmodified files"); |
| 46 | + sub->add_flag("-B,--break-rewrites", m_break_rewrites_flag, "Detect file rewrites"); |
| 47 | + |
| 48 | + sub->add_option("-U,--unified", m_context_lines, "Lines of context"); |
| 49 | + sub->add_option("--inter-hunk-context", m_interhunk_lines, "Context between hunks"); |
| 50 | + sub->add_option("--abbrev", m_abbrev, "Abbreviation length for object names") |
| 51 | + ->expected(0,1); |
| 52 | + |
| 53 | + sub->add_flag("--color", m_colour_flag, "Show colored diff"); |
| 54 | + sub->add_flag("--no-color", m_no_colour_flag, "Turn off colored diff"); |
| 55 | + |
| 56 | + sub->callback([this]() { this->run(); }); |
| 57 | +} |
| 58 | + |
| 59 | +void diff_subcommand::print_stats(const diff_wrapper& diff, bool use_colour) |
| 60 | +{ |
| 61 | + git_diff_stats_format_t format; |
| 62 | + if (m_stat_flag) |
| 63 | + { |
| 64 | + format = GIT_DIFF_STATS_FULL; |
| 65 | + } |
| 66 | + if (m_shortstat_flag) |
| 67 | + { |
| 68 | + format = GIT_DIFF_STATS_SHORT; |
| 69 | + } |
| 70 | + if (m_numstat_flag) |
| 71 | + { |
| 72 | + format = GIT_DIFF_STATS_NUMBER; |
| 73 | + } |
| 74 | + if (m_summary_flag) |
| 75 | + { |
| 76 | + format = GIT_DIFF_STATS_INCLUDE_SUMMARY; |
| 77 | + } |
| 78 | + |
| 79 | + auto stats = diff.get_stats(); |
| 80 | + auto buf = stats.to_buf(format, 80); |
| 81 | + |
| 82 | + if (use_colour && m_stat_flag) |
| 83 | + { |
| 84 | + // Add colors to + and - characters |
| 85 | + std::string output(buf.ptr); |
| 86 | + bool in_parentheses = false; |
| 87 | + for (char c : output) |
| 88 | + { |
| 89 | + if (c == '(') |
| 90 | + { |
| 91 | + in_parentheses = true; |
| 92 | + std::cout << c; |
| 93 | + } |
| 94 | + else if (c == ')') |
| 95 | + { |
| 96 | + in_parentheses = false; |
| 97 | + std::cout << c; |
| 98 | + } |
| 99 | + else if (c == '+' && !in_parentheses) |
| 100 | + { |
| 101 | + std::cout << termcolor::green << '+' << termcolor::reset; |
| 102 | + } |
| 103 | + else if (c == '-' && !in_parentheses) |
| 104 | + { |
| 105 | + std::cout << termcolor::red << '-' << termcolor::reset; |
| 106 | + } |
| 107 | + else |
| 108 | + { |
| 109 | + std::cout << c; |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + else |
| 114 | + { |
| 115 | + std::cout << buf.ptr; |
| 116 | + } |
| 117 | + |
| 118 | + git_buf_dispose(&buf); |
| 119 | +} |
| 120 | + |
| 121 | +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) |
| 122 | +{ |
| 123 | + bool* use_colour = reinterpret_cast<bool*>(payload); |
| 124 | + |
| 125 | + if (*use_colour) |
| 126 | + { |
| 127 | + switch (line->origin) { |
| 128 | + case GIT_DIFF_LINE_ADDITION: std::cout << termcolor::green; break; |
| 129 | + case GIT_DIFF_LINE_DELETION: std::cout << termcolor::red; break; |
| 130 | + case GIT_DIFF_LINE_ADD_EOFNL: std::cout << termcolor::green; break; |
| 131 | + case GIT_DIFF_LINE_DEL_EOFNL: std::cout << termcolor::red; break; |
| 132 | + case GIT_DIFF_LINE_FILE_HDR: std::cout << termcolor::bold; break; |
| 133 | + case GIT_DIFF_LINE_HUNK_HDR: std::cout << termcolor::cyan; break; |
| 134 | + default: break; |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + std::cout << line->origin << std::string_view(line->content, line->content_len); |
| 139 | + if (use_colour) |
| 140 | + { |
| 141 | + std::cout << termcolor::reset; |
| 142 | + } |
| 143 | + |
| 144 | + return 0; |
| 145 | +} |
| 146 | + |
| 147 | +void diff_subcommand::print_diff(diff_wrapper& diff, bool use_colour) |
| 148 | +{ |
| 149 | + if (m_stat_flag || m_shortstat_flag || m_numstat_flag || m_summary_flag) |
| 150 | + { |
| 151 | + print_stats(diff, use_colour); |
| 152 | + return; |
| 153 | + } |
| 154 | + |
| 155 | + if (m_find_renames_flag || m_find_copies_flag || m_find_copies_harder_flag || m_break_rewrites_flag) |
| 156 | + { |
| 157 | + git_diff_find_options find_opts; |
| 158 | + git_diff_find_options_init(&find_opts, GIT_DIFF_FIND_OPTIONS_VERSION); |
| 159 | + |
| 160 | + if (m_find_renames_flag) |
| 161 | + { |
| 162 | + find_opts.flags |= GIT_DIFF_FIND_RENAMES; |
| 163 | + find_opts.rename_threshold = m_rename_threshold; |
| 164 | + } |
| 165 | + if (m_find_copies_flag) |
| 166 | + { |
| 167 | + find_opts.flags |= GIT_DIFF_FIND_COPIES; |
| 168 | + find_opts.copy_threshold = m_copy_threshold; |
| 169 | + } |
| 170 | + if (m_find_copies_harder_flag) |
| 171 | + { |
| 172 | + find_opts.flags |= GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED; |
| 173 | + } |
| 174 | + if (m_break_rewrites_flag) |
| 175 | + { |
| 176 | + find_opts.flags |= GIT_DIFF_FIND_REWRITES; |
| 177 | + } |
| 178 | + |
| 179 | + diff.find_similar(&find_opts); |
| 180 | + } |
| 181 | + |
| 182 | + git_diff_format_t format = GIT_DIFF_FORMAT_PATCH; |
| 183 | + if (m_name_only_flag) |
| 184 | + { |
| 185 | + format = GIT_DIFF_FORMAT_NAME_ONLY; |
| 186 | + } |
| 187 | + else if (m_name_status_flag) |
| 188 | + { |
| 189 | + format = GIT_DIFF_FORMAT_NAME_STATUS; |
| 190 | + } |
| 191 | + else if (m_raw_flag) |
| 192 | + { |
| 193 | + format = GIT_DIFF_FORMAT_RAW; |
| 194 | + } |
| 195 | + |
| 196 | + diff.print(format, colour_printer, &use_colour); |
| 197 | +} |
| 198 | + |
| 199 | +diff_wrapper compute_diff_no_index(std::vector<std::string> files, git_diff_options& diffopts) //std::pair<buf_wrapper, diff_wrapper> |
| 200 | +{ |
| 201 | + if (files.size() != 2) |
| 202 | + { |
| 203 | + throw git_exception("two files should be provided as arguments", -1); //TODO: check error + code |
| 204 | + } |
| 205 | + |
| 206 | + git_diff_options_init(&diffopts, GIT_DIFF_OPTIONS_VERSION); |
| 207 | + |
| 208 | + std::string file1_str = read_file(files[0]); |
| 209 | + std::string file2_str = read_file(files[1]); |
| 210 | + |
| 211 | + if (file1_str.empty()) |
| 212 | + { |
| 213 | + throw git_exception("file " + files[0] + " cannot be read", -1); //TODO: check error + code |
| 214 | + } |
| 215 | + if (file2_str.empty()) |
| 216 | + { |
| 217 | + throw git_exception("file " + files[1] + " cannot be read", -1); //TODO: check error + code |
| 218 | + } |
| 219 | + |
| 220 | + auto patch = patch_wrapper::patch_from_files(files[0], file1_str, files[1], file2_str, &diffopts); |
| 221 | + auto buf = patch.to_buf(); |
| 222 | + auto diff = diff_wrapper::diff_from_buffer(buf); |
| 223 | + |
| 224 | + git_buf_dispose(&buf); |
| 225 | + |
| 226 | + return diff; |
| 227 | +} |
| 228 | + |
| 229 | +void diff_subcommand::run() |
| 230 | +{ |
| 231 | + git_diff_options diffopts; |
| 232 | + git_diff_options_init(&diffopts, GIT_DIFF_OPTIONS_VERSION); |
| 233 | + |
| 234 | + bool use_colour = false; |
| 235 | + if (m_colour_flag) |
| 236 | + { |
| 237 | + use_colour = true; |
| 238 | + } |
| 239 | + if (m_no_colour_flag) |
| 240 | + { |
| 241 | + use_colour = false; |
| 242 | + } |
| 243 | + |
| 244 | + if (m_no_index_flag) |
| 245 | + { |
| 246 | + auto diff = compute_diff_no_index(m_files, diffopts); |
| 247 | + diff_subcommand::print_diff(diff, use_colour); |
| 248 | + } |
| 249 | + else |
| 250 | + { |
| 251 | + auto directory = get_current_git_path(); |
| 252 | + auto repo = repository_wrapper::open(directory); |
| 253 | + |
| 254 | + diffopts.context_lines = m_context_lines; |
| 255 | + diffopts.interhunk_lines = m_interhunk_lines; |
| 256 | + diffopts.id_abbrev = m_abbrev; |
| 257 | + |
| 258 | + if (m_reverse_flag) { diffopts.flags |= GIT_DIFF_REVERSE; } |
| 259 | + if (m_text_flag) { diffopts.flags |= GIT_DIFF_FORCE_TEXT; } |
| 260 | + if (m_ignore_space_at_eol_flag) { diffopts.flags |= GIT_DIFF_IGNORE_WHITESPACE_EOL; } |
| 261 | + if (m_ignore_space_change_flag) { diffopts.flags |= GIT_DIFF_IGNORE_WHITESPACE_CHANGE; } |
| 262 | + if (m_ignore_all_space_flag) { diffopts.flags |= GIT_DIFF_IGNORE_WHITESPACE; } |
| 263 | + if (m_untracked_flag) { diffopts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; } |
| 264 | + if (m_patience_flag) { diffopts.flags |= GIT_DIFF_PATIENCE; } |
| 265 | + if (m_minimal_flag) { diffopts.flags |= GIT_DIFF_MINIMAL; } |
| 266 | + |
| 267 | + std::optional<tree_wrapper> tree1; |
| 268 | + std::optional<tree_wrapper> tree2; |
| 269 | + |
| 270 | + // TODO: throw error if m_files.size() > 2 |
| 271 | + if (m_files.size() >= 1) |
| 272 | + { |
| 273 | + tree1 = repo.treeish_to_tree(m_files[0]); |
| 274 | + } |
| 275 | + if (m_files.size() ==2) |
| 276 | + { |
| 277 | + tree2 = repo.treeish_to_tree(m_files[1]); |
| 278 | + } |
| 279 | + |
| 280 | + auto diff = [&repo, &tree1, &tree2, &diffopts, this]() |
| 281 | + { |
| 282 | + if (tree1.has_value() && tree2.has_value()) |
| 283 | + { |
| 284 | + return repo.diff_tree_to_tree(std::move(tree1.value()), std::move(tree2.value()), &diffopts); |
| 285 | + } |
| 286 | + else if (m_cached_flag) |
| 287 | + { |
| 288 | + if (m_cached_flag || !tree1) |
| 289 | + { |
| 290 | + tree1 = repo.treeish_to_tree("HEAD"); |
| 291 | + } |
| 292 | + return repo.diff_tree_to_index(std::move(tree1.value()), std::nullopt, &diffopts); |
| 293 | + } |
| 294 | + else if (tree1) |
| 295 | + { |
| 296 | + return repo.diff_tree_to_workdir_with_index(std::move(tree1.value()), &diffopts); |
| 297 | + } |
| 298 | + else |
| 299 | + { |
| 300 | + return repo.diff_index_to_workdir(std::nullopt, &diffopts); |
| 301 | + } |
| 302 | + }(); |
| 303 | + |
| 304 | + diff_subcommand::print_diff(diff, use_colour); |
| 305 | + } |
| 306 | +} |
0 commit comments