Skip to content

Commit 2acbb53

Browse files
committed
Add diff subcommand
1 parent 76f2107 commit 2acbb53

18 files changed

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

0 commit comments

Comments
 (0)