diff --git a/src/subcommand/diff_subcommand.cpp b/src/subcommand/diff_subcommand.cpp index 1ed8fb1..dc4ba41 100644 --- a/src/subcommand/diff_subcommand.cpp +++ b/src/subcommand/diff_subcommand.cpp @@ -12,7 +12,8 @@ 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"); + 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"); @@ -33,15 +34,16 @@ diff_subcommand::diff_subcommand(const libgit2_object&, CLI::App& app) 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"); - // TODO: add the following flags after the "move" subcommand has been implemented (needed for the tests) - // sub->add_option("-M,--find-renames", m_rename_threshold, "Detect renames") - // ->expected(0,1) - // ->each([this](const std::string&) { m_find_renames_flag = true; }); - // sub->add_option("-C,--find-copies", m_copy_threshold, "Detect copies") - // ->expected(0,1) - // ->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("-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"); @@ -142,7 +144,6 @@ static int colour_printer([[maybe_unused]] const git_diff_delta* delta, [[maybe_ bool use_colour = *reinterpret_cast(payload); // Only print origin for context/addition/deletion lines - // For other line types, content already includes everything bool print_origin = (line->origin == GIT_DIFF_LINE_CONTEXT || line->origin == GIT_DIFF_LINE_ADDITION || line->origin == GIT_DIFF_LINE_DELETION); @@ -172,6 +173,31 @@ static int colour_printer([[maybe_unused]] const git_diff_delta* delta, [[maybe_ 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; } @@ -183,33 +209,30 @@ void diff_subcommand::print_diff(diff_wrapper& diff, bool use_colour) return; } - // TODO: add the following flags after the "move" subcommand has been implemented (needed for the tests) - // 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(&find_opts, GIT_DIFF_FIND_OPTIONS_VERSION); - - // if (m_find_renames_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); - // } + 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 = (uint16_t)m_rename_threshold; + } + if (m_find_copies_flag) + { + find_opts.flags |= GIT_DIFF_FIND_COPIES; + find_opts.copy_threshold = (uint16_t)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) @@ -228,7 +251,7 @@ void diff_subcommand::print_diff(diff_wrapper& diff, bool use_colour) diff.print(format, colour_printer, &use_colour); } -diff_wrapper compute_diff_no_index(std::vector files, git_diff_options& diffopts) //std::pair +diff_wrapper compute_diff_no_index(std::vector files, git_diff_options& diffopts) { if (files.size() != 2) { @@ -242,11 +265,11 @@ diff_wrapper compute_diff_no_index(std::vector files, git_diff_opti if (file1_str.empty()) { - throw git_exception("Cannot read file: " + files[0], git2cpp_error_code::GENERIC_ERROR); //TODO: check error code with git + 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); //TODO: check error code with git + 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); @@ -263,6 +286,10 @@ void diff_subcommand::run() git_diff_options diffopts; git_diff_options_init(&diffopts, GIT_DIFF_OPTIONS_VERSION); + std::cerr << "DEBUG cached=" << m_cached_flag + << " no_index=" << m_no_index_flag + << " files=" << m_files.size() << "\n"; + bool use_colour = false; if (m_no_colour_flag) { @@ -280,6 +307,11 @@ void diff_subcommand::run() 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); @@ -302,11 +334,14 @@ void diff_subcommand::run() 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; - // TODO: throw error if m_files.size() > 2 if (m_files.size() >= 1) { tree1 = repo.treeish_to_tree(m_files[0]); @@ -324,7 +359,7 @@ void diff_subcommand::run() } else if (m_cached_flag) { - if (m_cached_flag || !tree1) + if (!tree1) { tree1 = repo.treeish_to_tree("HEAD"); } diff --git a/src/subcommand/diff_subcommand.hpp b/src/subcommand/diff_subcommand.hpp index 5c2c23f..adda75a 100644 --- a/src/subcommand/diff_subcommand.hpp +++ b/src/subcommand/diff_subcommand.hpp @@ -38,16 +38,16 @@ class diff_subcommand bool m_patience_flag = false; bool m_minimal_flag = false; - // int m_rename_threshold = 50; - // bool m_find_renames_flag = false; - // int m_copy_threshold = 50; - // bool m_find_copies_flag = false; - // bool m_find_copies_harder_flag = false; - // bool m_break_rewrites_flag = false; - - int m_context_lines = 3; - int m_interhunk_lines = 0; - int m_abbrev = 7; + uint m_rename_threshold = 50; + bool m_find_renames_flag = false; + uint m_copy_threshold = 50; + bool m_find_copies_flag = false; + bool m_find_copies_harder_flag = false; + bool m_break_rewrites_flag = false; + + uint m_context_lines = 3; + uint m_interhunk_lines = 0; + uint m_abbrev = 7; bool m_colour_flag = true; bool m_no_colour_flag = false; diff --git a/test/conftest.py b/test/conftest.py index ea11f67..7ed4a75 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,11 +4,12 @@ import pytest -GIT2CPP_TEST_WASM = os.getenv('GIT2CPP_TEST_WASM') == "1" +GIT2CPP_TEST_WASM = os.getenv("GIT2CPP_TEST_WASM") == "1" if GIT2CPP_TEST_WASM: from .conftest_wasm import * + # Fixture to run test in current tmp_path @pytest.fixture def run_in_tmp_path(tmp_path): @@ -21,9 +22,10 @@ def run_in_tmp_path(tmp_path): @pytest.fixture(scope="session") def git2cpp_path(): if GIT2CPP_TEST_WASM: - return 'git2cpp' + return "git2cpp" else: - return Path(__file__).parent.parent / 'build' / 'git2cpp' + return Path(__file__).parent.parent / "build" / "git2cpp" + @pytest.fixture def xtl_clone(git2cpp_path, tmp_path, run_in_tmp_path): @@ -39,10 +41,28 @@ def commit_env_config(monkeypatch): "GIT_AUTHOR_NAME": "Jane Doe", "GIT_AUTHOR_EMAIL": "jane.doe@blabla.com", "GIT_COMMITTER_NAME": "Jane Doe", - "GIT_COMMITTER_EMAIL": "jane.doe@blabla.com" + "GIT_COMMITTER_EMAIL": "jane.doe@blabla.com", } for key, value in config.items(): if GIT2CPP_TEST_WASM: subprocess.run(["export", f"{key}='{value}'"], check=True) else: monkeypatch.setenv(key, value) + + +@pytest.fixture +def repo_init_with_commit(commit_env_config, git2cpp_path, tmp_path, run_in_tmp_path): + cmd_init = [git2cpp_path, "init", "."] + p_init = subprocess.run(cmd_init, capture_output=True, cwd=tmp_path, text=True) + assert p_init.returncode == 0 + + p = tmp_path / "initial.txt" + p.write_text("initial") + + cmd_add = [git2cpp_path, "add", "initial.txt"] + p_add = subprocess.run(cmd_add, capture_output=True, cwd=tmp_path, text=True) + assert p_add.returncode == 0 + + cmd_commit = [git2cpp_path, "commit", "-m", "Initial commit"] + p_commit = subprocess.run(cmd_commit, capture_output=True, cwd=tmp_path, text=True) + assert p_commit.returncode == 0 diff --git a/test/test_diff.py b/test/test_diff.py index 6dd5f1a..c0f907f 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -1,5 +1,6 @@ import re import subprocess +from sys import stderr import pytest @@ -11,51 +12,53 @@ def test_diff_nogit(git2cpp_path, tmp_path): assert "repository" in p.stderr.lower() or "not a git" in p.stderr.lower() -def test_diff_working_directory(xtl_clone, git2cpp_path, tmp_path): - xtl_path = tmp_path / "xtl" +def test_diff_working_directory(repo_init_with_commit, git2cpp_path, tmp_path): + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - original_content = readme.read_text() - readme.write_text(original_content + "\nNew line added") + original_content = initial_file.read_text() + initial_file.write_text(original_content + "\nNew line added") cmd = [git2cpp_path, "diff"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert "README.md" in p.stdout - assert "New line added" in p.stdout # should be "+New line added" + assert "initial.txt" in p.stdout + assert "+New line added" in p.stdout @pytest.mark.parametrize("cached_flag", ["--cached", "--staged"]) -def test_diff_cached(xtl_clone, git2cpp_path, tmp_path, cached_flag): - xtl_path = tmp_path / "xtl" +def test_diff_cached(repo_init_with_commit, git2cpp_path, tmp_path, cached_flag): + assert (tmp_path / "initial.txt").exists() - new_file = xtl_path / "new_file.txt" + new_file = tmp_path / "new_file.txt" new_file.write_text("Hello, world!") cmd_add = [git2cpp_path, "add", "new_file.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_diff = [git2cpp_path, "diff", cached_flag] - p_diff = subprocess.run(cmd_diff, capture_output=True, cwd=xtl_path, text=True) + p_diff = subprocess.run(cmd_diff, capture_output=True, cwd=tmp_path, text=True) assert p_diff.returncode == 0 assert "new_file.txt" in p_diff.stdout assert "+Hello, world!" in p_diff.stdout -def test_diff_two_commits(xtl_clone, commit_env_config, git2cpp_path, tmp_path): - xtl_path = tmp_path / "xtl" +def test_diff_two_commits( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path +): + assert (tmp_path / "initial.txt").exists() - new_file = xtl_path / "new_file.txt" + new_file = tmp_path / "new_file.txt" new_file.write_text("Hello, world!") cmd_add = [git2cpp_path, "add", "new_file.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "new commit"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) cmd_diff = [git2cpp_path, "diff", "HEAD~1", "HEAD"] - p_diff = subprocess.run(cmd_diff, capture_output=True, cwd=xtl_path, text=True) + p_diff = subprocess.run(cmd_diff, capture_output=True, cwd=tmp_path, text=True) assert p_diff.returncode == 0 assert "new_file.txt" in p_diff.stdout assert "+Hello, world!" in p_diff.stdout @@ -75,208 +78,212 @@ def test_diff_no_index(git2cpp_path, tmp_path): assert "+Python" in p.stdout -def test_diff_stat(xtl_clone, git2cpp_path, tmp_path): - xtl_path = tmp_path / "xtl" +def test_diff_stat(repo_init_with_commit, git2cpp_path, tmp_path): + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - readme.write_text("Modified content\n") + initial_file.write_text("Modified content\n") cmd = [git2cpp_path, "diff", "--stat"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert "README.md" in p.stdout + assert "initial.txt" in p.stdout assert "1 file changed, 1 insertion(+)" in p.stdout assert "Modified content" not in p.stdout -def test_diff_shortstat(xtl_clone, git2cpp_path, tmp_path): +def test_diff_shortstat(repo_init_with_commit, git2cpp_path, tmp_path): """Test diff with --shortstat (last line of --stat only)""" - xtl_path = tmp_path / "xtl" + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - readme.write_text("Modified content\n") + initial_file.write_text("Modified content\n") cmd = [git2cpp_path, "diff", "--shortstat"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert "README.md" not in p.stdout assert "1 file changed, 1 insertion(+)" in p.stdout assert "Modified content" not in p.stdout -def test_diff_numstat(xtl_clone, git2cpp_path, tmp_path): +def test_diff_numstat(repo_init_with_commit, git2cpp_path, tmp_path): """Test diff with --numstat (machine-friendly stat)""" - xtl_path = tmp_path / "xtl" + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - readme.write_text("Modified content\n") + initial_file.write_text("Modified content\n") cmd = [git2cpp_path, "diff", "--numstat"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert "README.md" in p.stdout + assert "initial.txt" in p.stdout assert bool(re.search("1 [0-9]*", p.stdout)) assert "Modified content" not in p.stdout -def test_diff_summary(xtl_clone, git2cpp_path, tmp_path): +def test_diff_summary(repo_init_with_commit, git2cpp_path, tmp_path): """Test diff with --summary""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - new_file = xtl_path / "newfile.txt" + new_file = tmp_path / "newfile.txt" new_file.write_text("New content") cmd_add = [git2cpp_path, "add", "newfile.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd = [git2cpp_path, "diff", "--cached", "--summary"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert "newfile.txt" in p.stdout assert "+" not in p.stdout -def test_diff_name_only(xtl_clone, git2cpp_path, tmp_path): - xtl_path = tmp_path / "xtl" +def test_diff_name_only(repo_init_with_commit, git2cpp_path, tmp_path): + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - (xtl_path / "README.md").write_text("Modified") + initial_file.write_text("Modified") cmd = [git2cpp_path, "diff", "--name-only"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert p.stdout == "README.md\n" + assert p.stdout == "initial.txt\n" assert "+" not in p.stdout -def test_diff_name_status(xtl_clone, git2cpp_path, tmp_path): - xtl_path = tmp_path / "xtl" +def test_diff_name_status(repo_init_with_commit, git2cpp_path, tmp_path): + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - (xtl_path / "README.md").write_text("Modified") + initial_file.write_text("Modified") cmd = [git2cpp_path, "diff", "--name-status"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert p.stdout == "M\tREADME.md\n" + assert p.stdout == "M\tinitial.txt\n" -def test_diff_raw(xtl_clone, git2cpp_path, tmp_path): +def test_diff_raw(repo_init_with_commit, git2cpp_path, tmp_path): """Test diff with --raw format""" - xtl_path = tmp_path / "xtl" + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - readme.write_text("Modified") + initial_file.write_text("Modified") cmd = [git2cpp_path, "diff", "--raw"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert "M\tREADME.md" in p.stdout + assert "M\tinitial.txt" in p.stdout assert bool(re.search(":[0-9]*", p.stdout)) -def test_diff_reverse(xtl_clone, git2cpp_path, tmp_path): - xtl_path = tmp_path / "xtl" +def test_diff_reverse(repo_init_with_commit, git2cpp_path, tmp_path): + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - original = readme.read_text() - readme.write_text(original + "\nAdded line") + original = initial_file.read_text() + initial_file.write_text(original + "\nAdded line") cmd_normal = [git2cpp_path, "diff"] - p_normal = subprocess.run(cmd_normal, capture_output=True, cwd=xtl_path, text=True) + p_normal = subprocess.run(cmd_normal, capture_output=True, cwd=tmp_path, text=True) assert p_normal.returncode == 0 assert "+Added line" in p_normal.stdout cmd_reverse = [git2cpp_path, "diff", "-R"] p_reverse = subprocess.run( - cmd_reverse, capture_output=True, cwd=xtl_path, text=True + cmd_reverse, capture_output=True, cwd=tmp_path, text=True ) assert p_reverse.returncode == 0 assert "-Added line" in p_reverse.stdout @pytest.mark.parametrize("text_flag", ["-a", "--text"]) -def test_diff_text(xtl_clone, commit_env_config, git2cpp_path, tmp_path, text_flag): +def test_diff_text( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, text_flag +): """Test diff with -a/--text (treat all files as text)""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - binary_file = xtl_path / "binary.bin" + binary_file = tmp_path / "binary.bin" binary_file.write_bytes(b"\x00\x01\x02\x03") cmd_add = [git2cpp_path, "add", "binary.bin"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "add binary"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) binary_file.write_bytes(b"\x00\x01\x02\x04") cmd_text = [git2cpp_path, "diff", text_flag] - p = subprocess.run(cmd_text, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd_text, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert "binary.bin" in p.stdout assert "@@" in p.stdout -def test_diff_ignore_space_at_eol(xtl_clone, git2cpp_path, tmp_path): +def test_diff_ignore_space_at_eol(repo_init_with_commit, git2cpp_path, tmp_path): """Test diff with --ignore-space-at-eol""" - xtl_path = tmp_path / "xtl" + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - original = readme.read_text() + original = initial_file.read_text() # Add trailing spaces at end of line - readme.write_text(original.rstrip() + " \n") + initial_file.write_text(original.rstrip() + " \n") cmd = [git2cpp_path, "diff", "--ignore-space-at-eol"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert p.stdout == "" @pytest.mark.parametrize("space_change_flag", ["-b", "--ignore-space-change"]) def test_diff_ignore_space_change( - xtl_clone, commit_env_config, git2cpp_path, tmp_path, space_change_flag + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, space_change_flag ): """Test diff with -b/--ignore-space-change""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - test_file = xtl_path / "test.txt" + test_file = tmp_path / "test.txt" test_file.write_text("Hello world\n") cmd_add = [git2cpp_path, "add", "test.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "test"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) # Change spacing test_file.write_text("Hello world\n") cmd_diff = [git2cpp_path, "diff", space_change_flag] - p = subprocess.run(cmd_diff, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd_diff, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert p.stdout == "" @pytest.mark.parametrize("ignore_space_flag", ["-w", "--ignore-all-space"]) def test_diff_ignore_all_space( - xtl_clone, commit_env_config, git2cpp_path, tmp_path, ignore_space_flag + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, ignore_space_flag ): """Test diff with -w/--ignore-all-space""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - test_file = xtl_path / "test.txt" + test_file = tmp_path / "test.txt" test_file.write_text("Hello world\n") cmd_add = [git2cpp_path, "add", "test.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "test"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) test_file.write_text("Helloworld") cmd_diff = [git2cpp_path, "diff", ignore_space_flag] - p = subprocess.run(cmd_diff, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd_diff, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert p.stdout == "" @@ -286,7 +293,7 @@ def test_diff_ignore_all_space( [("-U0", 0), ("-U1", 1), ("-U5", 5), ("--unified=3", 3)], ) def test_diff_unified_context( - xtl_clone, + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, @@ -294,19 +301,19 @@ def test_diff_unified_context( context_lines, ): """Test diff with -U/--unified for context lines""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - test_file = xtl_path / "test.txt" + test_file = tmp_path / "test.txt" # Create a file with enough lines to see context differences test_file.write_text( "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10\n" ) cmd_add = [git2cpp_path, "add", "test.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "test"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) # Modify line 5 (middle of the file) test_file.write_text( @@ -315,7 +322,7 @@ def test_diff_unified_context( # Run diff with the parameterized flag cmd = [git2cpp_path, "diff", unified_context_flag] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert "test.txt" in p.stdout assert "MODIFIED LINE 5" in p.stdout @@ -365,19 +372,21 @@ def test_diff_unified_context( assert "Line 8" not in p.stdout or p.stdout.count("Line 8") == 0 -def test_diff_inter_hunk_context(xtl_clone, commit_env_config, git2cpp_path, tmp_path): +def test_diff_inter_hunk_context( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path +): """Test diff with --inter-hunk-context""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - test_file = xtl_path / "test.txt" + test_file = tmp_path / "test.txt" lines = [f"Line {i}\n" for i in range(1, 31)] test_file.write_text("".join(lines)) cmd_add = [git2cpp_path, "add", "test.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "test"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) # Modify two separate sections lines[4] = "Modified Line 5\n" @@ -386,7 +395,7 @@ def test_diff_inter_hunk_context(xtl_clone, commit_env_config, git2cpp_path, tmp # Test with small inter-hunk-context (should keep hunks separate) cmd_small = [git2cpp_path, "diff", "--inter-hunk-context=1"] - p_small = subprocess.run(cmd_small, capture_output=True, cwd=xtl_path, text=True) + p_small = subprocess.run(cmd_small, capture_output=True, cwd=tmp_path, text=True) assert p_small.returncode == 0 assert "Modified Line 5" in p_small.stdout assert "Modified Line 20" in p_small.stdout @@ -403,7 +412,7 @@ def test_diff_inter_hunk_context(xtl_clone, commit_env_config, git2cpp_path, tmp # Test with large inter-hunk-context (should merge hunks into one) cmd_large = [git2cpp_path, "diff", "--inter-hunk-context=15"] - p_large = subprocess.run(cmd_large, capture_output=True, cwd=xtl_path, text=True) + p_large = subprocess.run(cmd_large, capture_output=True, cwd=tmp_path, text=True) assert p_large.returncode == 0 assert "Modified Line 5" in p_large.stdout assert "Modified Line 20" in p_large.stdout @@ -428,18 +437,18 @@ def test_diff_inter_hunk_context(xtl_clone, commit_env_config, git2cpp_path, tmp ) -def test_diff_abbrev(xtl_clone, commit_env_config, git2cpp_path, tmp_path): +def test_diff_abbrev(repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path): """Test diff with --abbrev for object name abbreviation""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - test_file = xtl_path / "test.txt" + test_file = tmp_path / "test.txt" test_file.write_text("Original content\n") cmd_add = [git2cpp_path, "add", "test.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "initial commit"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) # Modify the file test_file.write_text("Modified content\n") @@ -447,20 +456,20 @@ def test_diff_abbrev(xtl_clone, commit_env_config, git2cpp_path, tmp_path): # Test default --abbrev cmd_default = [git2cpp_path, "diff", "--abbrev"] p_default = subprocess.run( - cmd_default, capture_output=True, cwd=xtl_path, text=True + cmd_default, capture_output=True, cwd=tmp_path, text=True ) assert p_default.returncode == 0 assert "test.txt" in p_default.stdout # Test --abbrev=7 (short hash) cmd_7 = [git2cpp_path, "diff", "--abbrev=7"] - p_7 = subprocess.run(cmd_7, capture_output=True, cwd=xtl_path, text=True) + p_7 = subprocess.run(cmd_7, capture_output=True, cwd=tmp_path, text=True) assert p_7.returncode == 0 assert "test.txt" in p_7.stdout # Test --abbrev=12 (longer hash) cmd_12 = [git2cpp_path, "diff", "--abbrev=12"] - p_12 = subprocess.run(cmd_12, capture_output=True, cwd=xtl_path, text=True) + p_12 = subprocess.run(cmd_12, capture_output=True, cwd=tmp_path, text=True) assert p_12.returncode == 0 assert "test.txt" in p_12.stdout @@ -480,41 +489,43 @@ def test_diff_abbrev(xtl_clone, commit_env_config, git2cpp_path, tmp_path): # Note: only checking if the output is a diff -def test_diff_patience(xtl_clone, commit_env_config, git2cpp_path, tmp_path): +def test_diff_patience( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path +): """Test diff with --patience algorithm""" - xtl_path = tmp_path / "xtl" + assert (tmp_path / "initial.txt").exists() - test_file = xtl_path / "test.txt" + test_file = tmp_path / "test.txt" test_file.write_text("Line 1\nLine 2\nLine 3\n") cmd_add = [git2cpp_path, "add", "test.txt"] - subprocess.run(cmd_add, cwd=xtl_path, check=True) + subprocess.run(cmd_add, cwd=tmp_path, check=True) cmd_commit = [git2cpp_path, "commit", "-m", "test"] - subprocess.run(cmd_commit, cwd=xtl_path, check=True) + subprocess.run(cmd_commit, cwd=tmp_path, check=True) test_file.write_text("Line 1\nNew Line\nLine 2\nLine 3\n") cmd = [git2cpp_path, "diff"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert "test.txt" in p.stdout assert "+New Line" in p.stdout # Note: only checking if the output is a diff -def test_diff_minimal(xtl_clone, git2cpp_path, tmp_path): +def test_diff_minimal(repo_init_with_commit, git2cpp_path, tmp_path): """Test diff with --minimal (spend extra time to find smallest diff)""" - xtl_path = tmp_path / "xtl" + initial_file = tmp_path / "initial.txt" + assert (initial_file).exists() - readme = xtl_path / "README.md" - original = readme.read_text() - readme.write_text(original + "\nExtra line\n") + original = initial_file.read_text() + initial_file.write_text(original + "\nExtra line\n") cmd = [git2cpp_path, "diff", "--minimal"] - p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert "README.md" in p.stdout + assert "initial.txt" in p.stdout assert "+Extra line" in p.stdout @@ -537,120 +548,158 @@ def test_diff_minimal(xtl_clone, git2cpp_path, tmp_path): # assert bool(re.search(ansi_escape, p.stdout)) -# TODO: add the following flags after the "move" subcommand has been implemented (needed for the tests) -# @pytest.mark.parametrize("renames_flag", ["-M", "--find-renames"]) -# def test_diff_find_renames(xtl_clone, git2cpp_path, tmp_path, renames_flag): -# """Test diff with -M/--find-renames""" -# xtl_path = tmp_path / "xtl" +@pytest.mark.parametrize("renames_flag", ["-M", "--find-renames"]) +def test_diff_find_renames(repo_init_with_commit, git2cpp_path, tmp_path, renames_flag): + """Test diff with -M/--find-renames""" + assert (tmp_path / "initial.txt").exists() -# old_file = xtl_path / "old_name.txt" -# old_file.write_text("Hello\n") + old_file = tmp_path / "old.txt" + old_file.write_text("Hello\n") -# cmd_add = [git2cpp_path, "add", "old_name.txt"] -# subprocess.run(cmd_add, cwd=xtl_path, check=True) + cmd_add = [git2cpp_path, "add", "old.txt"] + subprocess.run(cmd_add, cwd=tmp_path, check=True) -# cmd_commit = [git2cpp_path, "commit", "-m", "Add file"] -# subprocess.run(cmd_commit, cwd=xtl_path, check=True) + cmd_commit = [git2cpp_path, "commit", "-m", "Add file"] + subprocess.run(cmd_commit, cwd=tmp_path, check=True) -# new_file = xtl_path / "new_name.txt" -# old_file.rename(new_file) -# old_file.write_text("Goodbye\n") + cmd_mv = [git2cpp_path, "mv", "old.txt", "new.txt"] + subprocess.run(cmd_mv, cwd=tmp_path, check=True) -# cmd_add_all = [git2cpp_path, "add", "-A"] -# subprocess.run(cmd_add_all, cwd=xtl_path, check=True) + cmd = [git2cpp_path, "diff", "--cached", renames_flag] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + assert "similarity index" in p.stdout + assert "rename from old.txt" in p.stdout + assert "rename to new.txt" in p.stdout -# cmd = [git2cpp_path, "diff", "--cached", renames_flag] -# p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) -# assert p.returncode == 0 -# # assert "similarity index" in p.stdout -# # assert "rename from" in p.stdout -# assert "+++ b/new_name.txt" in p.stdout -# assert "--- a/old_name.txt" in p.stdout -# print("===\n", p.stdout, "===\n") +def test_diff_find_renames_with_threshold( + repo_init_with_commit, git2cpp_path, tmp_path +): + """Test diff with -M with threshold value""" + assert (tmp_path / "initial.txt").exists() -# def test_diff_find_renames_with_threshold(xtl_clone, git2cpp_path, tmp_path): -# """Test diff with -M with threshold value""" -# xtl_path = tmp_path / "xtl" + old_file = tmp_path / "old.txt" + old_file.write_text("Content\n") -# old_file = xtl_path / "old.txt" -# old_file.write_text("Content\n") + cmd_add = [git2cpp_path, "add", "old.txt"] + subprocess.run(cmd_add, cwd=tmp_path, check=True) -# cmd_add = [git2cpp_path, "add", "old.txt"] -# subprocess.run(cmd_add, cwd=xtl_path, check=True) + cmd_commit = [git2cpp_path, "commit", "-m", "Add file"] + subprocess.run(cmd_commit, cwd=tmp_path, check=True) -# cmd_commit = [git2cpp_path, "commit", "-m", "Add file"] -# subprocess.run(cmd_commit, cwd=xtl_path, check=True) + new_file = tmp_path / "new.txt" + old_file.rename(new_file) -# new_file = xtl_path / "new.txt" -# old_file.rename(new_file) + cmd_add_all = [git2cpp_path, "add", "-A"] + subprocess.run(cmd_add_all, cwd=tmp_path, check=True) -# cmd_add_all = [git2cpp_path, "add", "-A"] -# subprocess.run(cmd_add_all, cwd=xtl_path, check=True) + cmd = [git2cpp_path, "diff", "--cached", "-M60"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + assert "similarity index" in p.stdout + assert "rename from old.txt" in p.stdout + assert "rename to new.txt" in p.stdout -# cmd = [git2cpp_path, "diff", "--cached", "-M50"] -# p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) -# assert p.returncode == 0 -# print(p.stdout) # Doesn't do the same as the previous one. Why ??? +@pytest.mark.parametrize("copies_flag", ["-C", "--find-copies"]) +def test_diff_find_copies_from_modified( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, copies_flag +): + """Test diff with -C/--find-copies when source file is also modified""" + assert (tmp_path / "initial.txt").exists() -# @pytest.mark.parametrize("copies_flag", ["-C", "--find-copies"]) -# def test_diff_find_copies(xtl_clone, git2cpp_path, tmp_path, copies_flag): -# """Test diff with -C/--find-copies""" -# xtl_path = tmp_path / "xtl" + original_file = tmp_path / "original.txt" + original_file.write_text("Content to be copied\n") -# original_file = xtl_path / "original.txt" -# original_file.write_text("Content to be copied\n") + cmd_add = [git2cpp_path, "add", "original.txt"] + subprocess.run(cmd_add, cwd=tmp_path, check=True) -# cmd_add = [git2cpp_path, "add", "original.txt"] -# subprocess.run(cmd_add, cwd=xtl_path, check=True) + cmd_commit = [git2cpp_path, "commit", "-m", "add original file"] + subprocess.run(cmd_commit, cwd=tmp_path, check=True) -# copied_file = xtl_path / "copied.txt" -# copied_file.write_text("Content to be copied\n") + # Modify original.txt (this makes it a candidate for copy detection) + original_file.write_text("Content to be copied\nExtra line\n") + subprocess.run([git2cpp_path, "add", "original.txt"], cwd=tmp_path, check=True) -# cmd_add_copy = [git2cpp_path, "add", "copied.txt"] -# subprocess.run(cmd_add_copy, cwd=xtl_path, check=True) + # Create copy with original content + copied_file = tmp_path / "copied.txt" + copied_file.write_text("Content to be copied\n") + subprocess.run([git2cpp_path, "add", "copied.txt"], cwd=tmp_path, check=True) -# cmd = [git2cpp_path, "diff", "--cached", copies_flag] -# p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) -# assert p.returncode == 0 -# print(p.stdout) - - -# def test_diff_find_copies_with_threshold(xtl_clone, git2cpp_path, tmp_path): -# """Test diff with -C with threshold value""" -# xtl_path = tmp_path / "xtl" + cmd = [git2cpp_path, "diff", "--cached", copies_flag] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + assert "similarity index" in p.stdout + assert "copy from original.txt" in p.stdout + assert "copy to copied.txt" in p.stdout -# original_file = xtl_path / "original.txt" -# original_file.write_text("Content\n") -# cmd_add = [git2cpp_path, "add", "original.txt"] -# subprocess.run(cmd_add, cwd=xtl_path, check=True) +@pytest.mark.parametrize("copies_flag", ["-C", "--find-copies"]) +def test_diff_find_copies_harder( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, copies_flag +): + """Test diff with -C/--find-copies and --find-copies-harder for unmodified sources""" + assert (tmp_path / "initial.txt").exists() + + original_file = tmp_path / "original.txt" + original_file.write_text("Content to be copied\n") + + subprocess.run([git2cpp_path, "add", "original.txt"], cwd=tmp_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", "add original file"], + cwd=tmp_path, + check=True, + env=commit_env_config, + ) -# copied_file = xtl_path / "copied.txt" -# copied_file.write_text("Content to be copied\n") + # Create identical copy + copied_file = tmp_path / "copied.txt" + copied_file.write_text("Content to be copied\n") + subprocess.run([git2cpp_path, "add", "copied.txt"], cwd=tmp_path, check=True) -# cmd_add_copy = [git2cpp_path, "add", "copied.txt"] -# subprocess.run(cmd_add_copy, cwd=xtl_path, check=True) + # Use --find-copies-harder to detect copies from unmodified files + cmd = [git2cpp_path, "diff", "--cached", copies_flag, "--find-copies-harder"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) -# cmd = [git2cpp_path, "diff", "--cached", "-C50"] -# p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) -# assert p.returncode == 0 + assert p.returncode == 0 + assert "similarity index 100%" in p.stdout + assert "copy from original.txt" in p.stdout + assert "copy to copied.txt" in p.stdout -# def test_diff_find_copies_harder(xtl_clone, git2cpp_path, tmp_path): -# """Test diff with --find-copies-harder""" -# xtl_path = tmp_path / "xtl" +@pytest.mark.parametrize("copies_flag", ["-C50", "--find-copies=50"]) +def test_diff_find_copies_with_threshold( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, copies_flag +): + """Test diff with -C with custom threshold""" + assert (tmp_path / "initial.txt").exists() + + original_file = tmp_path / "original.txt" + original_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n" + original_file.write_text(original_content) + + subprocess.run([git2cpp_path, "add", "original.txt"], cwd=tmp_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", "add original file"], + cwd=tmp_path, + check=True, + env=commit_env_config, + ) -# test_file = xtl_path / "test.txt" -# test_file.write_text("Content\n") + # Create a partial copy (60% similar) + copied_file = tmp_path / "copied.txt" + copied_file.write_text("Line 1\nLine 2\nLine 3\nNew line\nAnother line\n") + subprocess.run([git2cpp_path, "add", "copied.txt"], cwd=tmp_path, check=True) -# cmd_add = [git2cpp_path, "add", "test.txt"] -# subprocess.run(cmd_add, cwd=xtl_path, check=True) + # With threshold of 50%, should detect copy + cmd = [git2cpp_path, "diff", "--cached", copies_flag, "--find-copies-harder"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) -# cmd = [git2cpp_path, "diff", "--cached", "--find-copies-harder"] -# p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) -# assert p.returncode == 0 + assert p.returncode == 0 + assert "similarity index" in p.stdout + assert "copy from original.txt" in p.stdout + assert "copy to copied.txt" in p.stdout # @pytest.mark.parametrize("break_rewrites_flag", ["-B", "--break-rewrites"]) @@ -673,4 +722,35 @@ def test_diff_minimal(xtl_clone, git2cpp_path, tmp_path): # cmd = [git2cpp_path, "diff", break_rewrites_flag] # p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) # assert p.returncode == 0 -# print(p.stdout) + + +def test_diff_refuses_more_than_two_treeish( + repo_init_with_commit, git2cpp_path, tmp_path +): + # HEAD exists thanks to repo_init_with_commit + p = subprocess.run( + [git2cpp_path, "diff", "HEAD", "HEAD", "HEAD"], + capture_output=True, + text=True, + cwd=tmp_path, + ) + assert p.returncode != 0 + assert "2 required but received 3" in p.stderr + + +def test_diff_cached_and_no_index_are_incompatible(git2cpp_path, tmp_path): + # Create two files to satisfy the --no-index path arguments + a = tmp_path / "a.txt" + b = tmp_path / "b.txt" + a.write_text("hello\n") + b.write_text("world\n") + + p = subprocess.run( + [git2cpp_path, "diff", "--cached", "--no-index", str(a), str(b)], + capture_output=True, + text=True, + cwd=tmp_path, + ) + + assert p.returncode != 0 + assert "--cached and --no-index are incompatible" in (p.stderr + p.stdout)