Skip to content

Commit 2670cf6

Browse files
committed
Add diff subcommand
1 parent 76f2107 commit 2670cf6

18 files changed

+1332
-5
lines changed

CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ set(GIT2CPP_SRC
4848
${GIT2CPP_SOURCE_DIR}/subcommand/checkout_subcommand.hpp
4949
${GIT2CPP_SOURCE_DIR}/subcommand/clone_subcommand.cpp
5050
${GIT2CPP_SOURCE_DIR}/subcommand/clone_subcommand.hpp
51+
${GIT2CPP_SOURCE_DIR}/subcommand/diff_subcommand.cpp
52+
${GIT2CPP_SOURCE_DIR}/subcommand/diff_subcommand.hpp
5153
${GIT2CPP_SOURCE_DIR}/subcommand/commit_subcommand.cpp
5254
${GIT2CPP_SOURCE_DIR}/subcommand/commit_subcommand.hpp
5355
${GIT2CPP_SOURCE_DIR}/subcommand/config_subcommand.cpp
@@ -96,10 +98,16 @@ set(GIT2CPP_SRC
9698
${GIT2CPP_SOURCE_DIR}/wrapper/commit_wrapper.hpp
9799
${GIT2CPP_SOURCE_DIR}/wrapper/config_wrapper.cpp
98100
${GIT2CPP_SOURCE_DIR}/wrapper/config_wrapper.hpp
101+
${GIT2CPP_SOURCE_DIR}/wrapper/diff_wrapper.cpp
102+
${GIT2CPP_SOURCE_DIR}/wrapper/diff_wrapper.hpp
103+
${GIT2CPP_SOURCE_DIR}/wrapper/diffstats_wrapper.cpp
104+
${GIT2CPP_SOURCE_DIR}/wrapper/diffstats_wrapper.hpp
99105
${GIT2CPP_SOURCE_DIR}/wrapper/index_wrapper.cpp
100106
${GIT2CPP_SOURCE_DIR}/wrapper/index_wrapper.hpp
101107
${GIT2CPP_SOURCE_DIR}/wrapper/object_wrapper.cpp
102108
${GIT2CPP_SOURCE_DIR}/wrapper/object_wrapper.hpp
109+
${GIT2CPP_SOURCE_DIR}/wrapper/patch_wrapper.cpp
110+
${GIT2CPP_SOURCE_DIR}/wrapper/patch_wrapper.hpp
103111
${GIT2CPP_SOURCE_DIR}/wrapper/rebase_wrapper.cpp
104112
${GIT2CPP_SOURCE_DIR}/wrapper/rebase_wrapper.hpp
105113
${GIT2CPP_SOURCE_DIR}/wrapper/refs_wrapper.cpp
@@ -114,6 +122,8 @@ set(GIT2CPP_SRC
114122
${GIT2CPP_SOURCE_DIR}/wrapper/signature_wrapper.hpp
115123
${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.cpp
116124
${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.hpp
125+
${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.cpp
126+
${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.hpp
117127
${GIT2CPP_SOURCE_DIR}/wrapper/wrapper_base.hpp
118128
${GIT2CPP_SOURCE_DIR}/main.cpp
119129
${GIT2CPP_SOURCE_DIR}/version.hpp

src/main.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "subcommand/clone_subcommand.hpp"
1212
#include "subcommand/commit_subcommand.hpp"
1313
#include "subcommand/config_subcommand.hpp"
14+
#include "subcommand/diff_subcommand.hpp"
1415
#include "subcommand/fetch_subcommand.hpp"
1516
#include "subcommand/init_subcommand.hpp"
1617
#include "subcommand/log_subcommand.hpp"
@@ -44,6 +45,7 @@ int main(int argc, char** argv)
4445
clone_subcommand clone(lg2_obj, app);
4546
commit_subcommand commit(lg2_obj, app);
4647
config_subcommand config(lg2_obj, app);
48+
diff_subcommand diff(lg2_obj, app);
4749
fetch_subcommand fetch(lg2_obj, app);
4850
reset_subcommand reset(lg2_obj, app);
4951
log_subcommand log(lg2_obj, app);

src/subcommand/checkout_subcommand.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ void checkout_subcommand::run()
2424

2525
if (repo.state() != GIT_REPOSITORY_STATE_NONE)
2626
{
27-
throw std::runtime_error("Cannot checkout, repository is in unexpected state");
27+
std::runtime_error("Cannot checkout, repository is in unexpected state");
2828
}
2929

3030
git_checkout_options options;

src/subcommand/diff_subcommand.cpp

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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

Comments
 (0)