From 81df0278b15dc4883adbe20c5fd61db1378375f1 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Fri, 14 Jul 2023 12:07:00 +1200 Subject: [PATCH] patch+LibDiff: Implement 'strip' of filenames when parsing patch Implement the patch '-p' / '--strip' option, which strips the given number of leading components from filenames parsed in the patch header. If not given this option defaults to the basename of that path. --- Tests/Utilities/TestPatch.cpp | 42 ++++++++++++++++++++++++++++ Userland/Libraries/LibDiff/Hunks.cpp | 36 ++++++++++++++++++++++-- Userland/Libraries/LibDiff/Hunks.h | 3 +- Userland/Utilities/patch.cpp | 4 ++- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/Tests/Utilities/TestPatch.cpp b/Tests/Utilities/TestPatch.cpp index e613afdf35..1b998b0295 100644 --- a/Tests/Utilities/TestPatch.cpp +++ b/Tests/Utilities/TestPatch.cpp @@ -107,3 +107,45 @@ TEST_CASE(basic_addition_patch_from_empty_file) EXPECT_FILE_EQ(MUST(String::formatted("{}/a", s_test_dir)), "1\n2\n3\n"); } + +TEST_CASE(strip_path_to_basename) +{ + PatchSetup setup; + + auto patch = R"( +--- /dev/null ++++ a/bunch/of/../folders/stripped/to/basename +@@ -0,0 +1 @@ ++Hello, friends! +)"sv; + + auto file = ""sv; + auto input = MUST(Core::File::open(MUST(String::formatted("{}/basename", s_test_dir)), Core::File::OpenMode::Write)); + MUST(input->write_until_depleted(file.bytes())); + + run_patch({}, patch, "patching file basename\n"sv); + + EXPECT_FILE_EQ(MUST(String::formatted("{}/basename", s_test_dir)), "Hello, friends!\n"); +} + +TEST_CASE(strip_path_partially) +{ + PatchSetup setup; + + auto patch = R"( +--- /dev/null ++++ a/bunch/of/../folders/stripped/to/basename +@@ -0,0 +1 @@ ++Hello, friends! +)"sv; + + MUST(Core::System::mkdir(MUST(String::formatted("{}/to", s_test_dir)), 0755)); + + auto file = ""sv; + auto input = MUST(Core::File::open(MUST(String::formatted("{}/to/basename", s_test_dir)), Core::File::OpenMode::Write)); + MUST(input->write_until_depleted(file.bytes())); + + run_patch({ "-p6" }, patch, "patching file to/basename\n"sv); + + EXPECT_FILE_EQ(MUST(String::formatted("{}/to/basename", s_test_dir)), "Hello, friends!\n"); +} diff --git a/Userland/Libraries/LibDiff/Hunks.cpp b/Userland/Libraries/LibDiff/Hunks.cpp index 7e35534313..af88fa6e4b 100644 --- a/Userland/Libraries/LibDiff/Hunks.cpp +++ b/Userland/Libraries/LibDiff/Hunks.cpp @@ -7,6 +7,7 @@ #include "Hunks.h" #include +#include namespace Diff { @@ -57,19 +58,48 @@ bool Parser::consume_line_number(size_t& number) return true; } -ErrorOr
Parser::parse_header() +ErrorOr Parser::parse_file_line(Optional const& strip_count) +{ + // FIXME: handle parsing timestamps as well. + auto path = consume_line(); + + // No strip count given. Default to basename of file. + if (!strip_count.has_value()) + return String::from_deprecated_string(LexicalPath::basename(path)); + + // NOTE: We cannot use LexicalPath::parts as we want to strip the non-canonicalized path. + auto const& parts = path.split_view('/'); + + // More components to strip than the filename has. Just pretend it is missing. + if (strip_count.value() >= parts.size()) + return String(); + + // Remove given number of leading components from the path. + size_t components = parts.size() - strip_count.value(); + + StringBuilder stripped_path; + for (size_t i = parts.size() - components; i < parts.size(); ++i) { + TRY(stripped_path.try_append(parts[i])); + if (i != parts.size() - 1) + TRY(stripped_path.try_append("/"sv)); + } + + return stripped_path.to_string(); +} + +ErrorOr
Parser::parse_header(Optional const& strip_count) { Header header; while (!is_eof()) { if (consume_specific("+++ ")) { - header.new_file_path = TRY(String::from_utf8(consume_line())); + header.new_file_path = TRY(parse_file_line(strip_count)); continue; } if (consume_specific("--- ")) { - header.old_file_path = TRY(String::from_utf8(consume_line())); + header.old_file_path = TRY(parse_file_line(strip_count)); continue; } diff --git a/Userland/Libraries/LibDiff/Hunks.h b/Userland/Libraries/LibDiff/Hunks.h index 35f35f47b1..51c0d7d46b 100644 --- a/Userland/Libraries/LibDiff/Hunks.h +++ b/Userland/Libraries/LibDiff/Hunks.h @@ -82,9 +82,10 @@ public: ErrorOr> parse_hunks(); - ErrorOr
parse_header(); + ErrorOr
parse_header(Optional const& strip_count); private: + ErrorOr parse_file_line(Optional const& strip_count); Optional consume_unified_location(); bool consume_line_number(size_t& number); }; diff --git a/Userland/Utilities/patch.cpp b/Userland/Utilities/patch.cpp index 6bb0681adb..17ef289ef8 100644 --- a/Userland/Utilities/patch.cpp +++ b/Userland/Utilities/patch.cpp @@ -30,9 +30,11 @@ static ErrorOr do_patch(StringView path_of_file_to_patch, Diff::Patch cons ErrorOr serenity_main(Main::Arguments arguments) { StringView directory; + Optional strip_count; Core::ArgsParser args_parser; args_parser.add_option(directory, "Change the working directory to before applying the patch file", "directory", 'd', "directory"); + args_parser.add_option(strip_count, "Strip given number of leading path components from file names (defaults as basename)", "strip", 'p', "count"); args_parser.parse(arguments); if (!directory.is_null()) @@ -45,7 +47,7 @@ ErrorOr serenity_main(Main::Arguments arguments) // FIXME: Support multiple patches in the patch file. Diff::Parser parser(patch_content); Diff::Patch patch; - patch.header = TRY(parser.parse_header()); + patch.header = TRY(parser.parse_header(strip_count)); patch.hunks = TRY(parser.parse_hunks()); // FIXME: Support adding/removing a file, and asking for file to patch as fallback otherwise.