Skip to content

Commit c3b5ce4

Browse files
committed
add --find-renames and --find-copies to the diff command
1 parent 054814f commit c3b5ce4

File tree

4 files changed

+398
-263
lines changed

4 files changed

+398
-263
lines changed

src/subcommand/diff_subcommand.cpp

Lines changed: 78 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ diff_subcommand::diff_subcommand(const libgit2_object&, CLI::App& app)
1212
{
1313
auto* sub = app.add_subcommand("diff", "Show changes between commits, commit and working tree, etc");
1414

15-
sub->add_option("<files>", m_files, "tree-ish objects to compare");
15+
sub->add_option("<files>", m_files, "tree-ish objects to compare")
16+
->expected(0, 2);
1617

1718
sub->add_flag("--stat", m_stat_flag, "Generate a diffstat");
1819
sub->add_flag("--shortstat", m_shortstat_flag, "Output only the last line of --stat");
@@ -33,15 +34,16 @@ diff_subcommand::diff_subcommand(const libgit2_object&, CLI::App& app)
3334
sub->add_flag("--patience", m_patience_flag, "Generate diff using patience algorithm");
3435
sub->add_flag("--minimal", m_minimal_flag, "Spend extra time to find smallest diff");
3536

36-
// TODO: add the following flags after the "move" subcommand has been implemented (needed for the tests)
37-
// sub->add_option("-M,--find-renames", m_rename_threshold, "Detect renames")
38-
// ->expected(0,1)
39-
// ->each([this](const std::string&) { m_find_renames_flag = true; });
40-
// sub->add_option("-C,--find-copies", m_copy_threshold, "Detect copies")
41-
// ->expected(0,1)
42-
// ->each([this](const std::string&) { m_find_copies_flag = true; });
43-
// sub->add_flag("--find-copies-harder", m_find_copies_harder_flag, "Detect copies from unmodified files");
44-
// sub->add_flag("-B,--break-rewrites", m_break_rewrites_flag, "Detect file rewrites");
37+
sub->add_option("-M,--find-renames", m_rename_threshold, "Detect renames")
38+
->expected(0,1)
39+
->default_val(50)
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+
->default_val(50)
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");
4547

4648
sub->add_option("-U,--unified", m_context_lines, "Lines of context");
4749
sub->add_option("--inter-hunk-context", m_interhunk_lines, "Context between hunks");
@@ -142,7 +144,6 @@ static int colour_printer([[maybe_unused]] const git_diff_delta* delta, [[maybe_
142144
bool use_colour = *reinterpret_cast<bool*>(payload);
143145

144146
// Only print origin for context/addition/deletion lines
145-
// For other line types, content already includes everything
146147
bool print_origin = (line->origin == GIT_DIFF_LINE_CONTEXT ||
147148
line->origin == GIT_DIFF_LINE_ADDITION ||
148149
line->origin == GIT_DIFF_LINE_DELETION);
@@ -172,6 +173,31 @@ static int colour_printer([[maybe_unused]] const git_diff_delta* delta, [[maybe_
172173
std::cout << termcolor::reset;
173174
}
174175

176+
// Print copy/rename headers ONLY after the "diff --git" line
177+
if (line->origin == GIT_DIFF_LINE_FILE_HDR)
178+
{
179+
if (delta->status == GIT_DELTA_COPIED)
180+
{
181+
if (use_colour)
182+
std::cout << termcolor::bold;
183+
std::cout << "similarity index " << delta->similarity << "%\n";
184+
std::cout << "copy from " << delta->old_file.path << "\n";
185+
std::cout << "copy to " << delta->new_file.path << "\n";
186+
if (use_colour)
187+
std::cout << termcolor::reset;
188+
}
189+
else if (delta->status == GIT_DELTA_RENAMED)
190+
{
191+
if (use_colour)
192+
std::cout << termcolor::bold;
193+
std::cout << "similarity index " << delta->similarity << "%\n";
194+
std::cout << "rename from " << delta->old_file.path << "\n";
195+
std::cout << "rename to " << delta->new_file.path << "\n";
196+
if (use_colour)
197+
std::cout << termcolor::reset;
198+
}
199+
}
200+
175201
return 0;
176202
}
177203

@@ -183,33 +209,30 @@ void diff_subcommand::print_diff(diff_wrapper& diff, bool use_colour)
183209
return;
184210
}
185211

186-
// TODO: add the following flags after the "move" subcommand has been implemented (needed for the tests)
187-
// if (m_find_renames_flag || m_find_copies_flag || m_find_copies_harder_flag || m_break_rewrites_flag)
188-
// {
189-
// git_diff_find_options find_opts;
190-
// git_diff_find_options_init(&find_opts, GIT_DIFF_FIND_OPTIONS_VERSION);
191-
192-
// if (m_find_renames_flag)
193-
// {
194-
// find_opts.flags |= GIT_DIFF_FIND_RENAMES;
195-
// find_opts.rename_threshold = m_rename_threshold;
196-
// }
197-
// if (m_find_copies_flag)
198-
// {
199-
// find_opts.flags |= GIT_DIFF_FIND_COPIES;
200-
// find_opts.copy_threshold = m_copy_threshold;
201-
// }
202-
// if (m_find_copies_harder_flag)
203-
// {
204-
// find_opts.flags |= GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED;
205-
// }
206-
// if (m_break_rewrites_flag)
207-
// {
208-
// find_opts.flags |= GIT_DIFF_FIND_REWRITES;
209-
// }
210-
211-
// diff.find_similar(&find_opts);
212-
// }
212+
if (m_find_renames_flag || m_find_copies_flag || m_find_copies_harder_flag || m_break_rewrites_flag)
213+
{
214+
git_diff_find_options find_opts = GIT_DIFF_FIND_OPTIONS_INIT;
215+
216+
if (m_find_renames_flag || m_find_copies_flag)
217+
{
218+
find_opts.flags |= GIT_DIFF_FIND_RENAMES;
219+
find_opts.rename_threshold = (uint16_t)m_rename_threshold;
220+
}
221+
if (m_find_copies_flag)
222+
{
223+
find_opts.flags |= GIT_DIFF_FIND_COPIES;
224+
find_opts.copy_threshold = (uint16_t)m_copy_threshold;
225+
}
226+
if (m_find_copies_harder_flag)
227+
{
228+
find_opts.flags |= GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED;
229+
}
230+
if (m_break_rewrites_flag)
231+
{
232+
find_opts.flags |= GIT_DIFF_FIND_REWRITES;
233+
}
234+
diff.find_similar(&find_opts);
235+
}
213236

214237
git_diff_format_t format = GIT_DIFF_FORMAT_PATCH;
215238
if (m_name_only_flag)
@@ -228,7 +251,7 @@ void diff_subcommand::print_diff(diff_wrapper& diff, bool use_colour)
228251
diff.print(format, colour_printer, &use_colour);
229252
}
230253

231-
diff_wrapper compute_diff_no_index(std::vector<std::string> files, git_diff_options& diffopts) //std::pair<buf_wrapper, diff_wrapper>
254+
diff_wrapper compute_diff_no_index(std::vector<std::string> files, git_diff_options& diffopts)
232255
{
233256
if (files.size() != 2)
234257
{
@@ -242,11 +265,11 @@ diff_wrapper compute_diff_no_index(std::vector<std::string> files, git_diff_opti
242265

243266
if (file1_str.empty())
244267
{
245-
throw git_exception("Cannot read file: " + files[0], git2cpp_error_code::GENERIC_ERROR); //TODO: check error code with git
268+
throw git_exception("Cannot read file: " + files[0], git2cpp_error_code::GENERIC_ERROR);
246269
}
247270
if (file2_str.empty())
248271
{
249-
throw git_exception("Cannot read file: " + files[1], git2cpp_error_code::GENERIC_ERROR); //TODO: check error code with git
272+
throw git_exception("Cannot read file: " + files[1], git2cpp_error_code::GENERIC_ERROR);
250273
}
251274

252275
auto patch = patch_wrapper::patch_from_files(files[0], file1_str, files[1], file2_str, &diffopts);
@@ -263,6 +286,10 @@ void diff_subcommand::run()
263286
git_diff_options diffopts;
264287
git_diff_options_init(&diffopts, GIT_DIFF_OPTIONS_VERSION);
265288

289+
std::cerr << "DEBUG cached=" << m_cached_flag
290+
<< " no_index=" << m_no_index_flag
291+
<< " files=" << m_files.size() << "\n";
292+
266293
bool use_colour = false;
267294
if (m_no_colour_flag)
268295
{
@@ -280,6 +307,11 @@ void diff_subcommand::run()
280307
use_colour = true;
281308
}
282309

310+
if (m_cached_flag && m_no_index_flag)
311+
{
312+
throw git_exception("--cached and --no-index are incompatible", git2cpp_error_code::BAD_ARGUMENT);
313+
}
314+
283315
if (m_no_index_flag)
284316
{
285317
auto diff = compute_diff_no_index(m_files, diffopts);
@@ -302,11 +334,14 @@ void diff_subcommand::run()
302334
if (m_untracked_flag) { diffopts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; }
303335
if (m_patience_flag) { diffopts.flags |= GIT_DIFF_PATIENCE; }
304336
if (m_minimal_flag) { diffopts.flags |= GIT_DIFF_MINIMAL; }
337+
if (m_find_copies_flag || m_find_copies_harder_flag || m_find_renames_flag)
338+
{
339+
diffopts.flags |= GIT_DIFF_INCLUDE_UNMODIFIED;
340+
}
305341

306342
std::optional<tree_wrapper> tree1;
307343
std::optional<tree_wrapper> tree2;
308344

309-
// TODO: throw error if m_files.size() > 2
310345
if (m_files.size() >= 1)
311346
{
312347
tree1 = repo.treeish_to_tree(m_files[0]);
@@ -324,7 +359,7 @@ void diff_subcommand::run()
324359
}
325360
else if (m_cached_flag)
326361
{
327-
if (m_cached_flag || !tree1)
362+
if (!tree1)
328363
{
329364
tree1 = repo.treeish_to_tree("HEAD");
330365
}

src/subcommand/diff_subcommand.hpp

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@ class diff_subcommand
3838
bool m_patience_flag = false;
3939
bool m_minimal_flag = false;
4040

41-
// int m_rename_threshold = 50;
42-
// bool m_find_renames_flag = false;
43-
// int m_copy_threshold = 50;
44-
// bool m_find_copies_flag = false;
45-
// bool m_find_copies_harder_flag = false;
46-
// bool m_break_rewrites_flag = false;
47-
48-
int m_context_lines = 3;
49-
int m_interhunk_lines = 0;
50-
int m_abbrev = 7;
41+
uint m_rename_threshold = 50;
42+
bool m_find_renames_flag = false;
43+
uint m_copy_threshold = 50;
44+
bool m_find_copies_flag = false;
45+
bool m_find_copies_harder_flag = false;
46+
bool m_break_rewrites_flag = false;
47+
48+
uint m_context_lines = 3;
49+
uint m_interhunk_lines = 0;
50+
uint m_abbrev = 7;
5151

5252
bool m_colour_flag = true;
5353
bool m_no_colour_flag = false;

test/conftest.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
import pytest
66

7-
GIT2CPP_TEST_WASM = os.getenv('GIT2CPP_TEST_WASM') == "1"
7+
GIT2CPP_TEST_WASM = os.getenv("GIT2CPP_TEST_WASM") == "1"
88

99
if GIT2CPP_TEST_WASM:
1010
from .conftest_wasm import *
1111

12+
1213
# Fixture to run test in current tmp_path
1314
@pytest.fixture
1415
def run_in_tmp_path(tmp_path):
@@ -21,9 +22,10 @@ def run_in_tmp_path(tmp_path):
2122
@pytest.fixture(scope="session")
2223
def git2cpp_path():
2324
if GIT2CPP_TEST_WASM:
24-
return 'git2cpp'
25+
return "git2cpp"
2526
else:
26-
return Path(__file__).parent.parent / 'build' / 'git2cpp'
27+
return Path(__file__).parent.parent / "build" / "git2cpp"
28+
2729

2830
@pytest.fixture
2931
def xtl_clone(git2cpp_path, tmp_path, run_in_tmp_path):
@@ -39,10 +41,28 @@ def commit_env_config(monkeypatch):
3941
"GIT_AUTHOR_NAME": "Jane Doe",
4042
"GIT_AUTHOR_EMAIL": "jane.doe@blabla.com",
4143
"GIT_COMMITTER_NAME": "Jane Doe",
42-
"GIT_COMMITTER_EMAIL": "jane.doe@blabla.com"
44+
"GIT_COMMITTER_EMAIL": "jane.doe@blabla.com",
4345
}
4446
for key, value in config.items():
4547
if GIT2CPP_TEST_WASM:
4648
subprocess.run(["export", f"{key}='{value}'"], check=True)
4749
else:
4850
monkeypatch.setenv(key, value)
51+
52+
53+
@pytest.fixture
54+
def repo_init_with_commit(commit_env_config, git2cpp_path, tmp_path, run_in_tmp_path):
55+
cmd_init = [git2cpp_path, "init", "."]
56+
p_init = subprocess.run(cmd_init, capture_output=True, cwd=tmp_path, text=True)
57+
assert p_init.returncode == 0
58+
59+
p = tmp_path / "initial.txt"
60+
p.write_text("initial")
61+
62+
cmd_add = [git2cpp_path, "add", "initial.txt"]
63+
p_add = subprocess.run(cmd_add, capture_output=True, cwd=tmp_path, text=True)
64+
assert p_add.returncode == 0
65+
66+
cmd_commit = [git2cpp_path, "commit", "-m", "Initial commit"]
67+
p_commit = subprocess.run(cmd_commit, capture_output=True, cwd=tmp_path, text=True)
68+
assert p_commit.returncode == 0

0 commit comments

Comments
 (0)