ladybird/Userland/Libraries/LibDiff/Applier.cpp
Shannon Booth 828d791a4f LibDiff: Add Diff::apply_patch
Given a set of lines from the file we are patching, and a patch itself,
this function will try and locate where in the file to apply that patch,
and write the result of patching that file (if successful) to the output
stream.
2023-07-13 10:29:30 +01:00

153 lines
5.4 KiB
C++

/*
* Copyright (c) 2023, Shannon Booth <shannon.ml.booth@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Stream.h>
#include <LibDiff/Applier.h>
#include <LibDiff/Hunks.h>
namespace Diff {
static size_t expected_line_number(HunkLocation const& location)
{
auto line = location.old_range.start_line;
// NOTE: This is to handle the case we are adding a file, e.g for a range such as:
// '@@ -0,0 +1,3 @@'
if (location.old_range.start_line == 0)
++line;
VERIFY(line != 0);
return line;
}
struct Location {
size_t line_number;
size_t fuzz { 0 };
ssize_t offset { 0 };
};
static Optional<Location> locate_hunk(Vector<StringView> const& content, Hunk const& hunk, ssize_t offset, size_t max_fuzz = 3)
{
// Make a first best guess at where the from-file range is telling us where the hunk should be.
size_t offset_guess = expected_line_number(hunk.location) - 1 + offset;
// If there's no lines surrounding this hunk - it will always succeed, so there is no point in checking any further.
if (hunk.location.old_range.number_of_lines == 0)
return Location { offset_guess, 0, 0 };
size_t patch_prefix_context = 0;
for (auto const& line : hunk.lines) {
if (line.operation != Line::Operation::Context)
break;
++patch_prefix_context;
}
size_t patch_suffix_context = 0;
for (auto const& line : hunk.lines.in_reverse()) {
if (line.operation != Line::Operation::Context)
break;
++patch_suffix_context;
}
size_t context = max(patch_prefix_context, patch_suffix_context);
// Look through the file trying to match the hunk for it. If we can't find anything anywhere in the file, then try and
// match the hunk by ignoring an increasing amount of context lines. The number of context lines that are ignored is
// called the 'fuzz'.
for (size_t fuzz = 0; fuzz <= max_fuzz; ++fuzz) {
auto suffix_fuzz = max(fuzz + patch_suffix_context - context, 0);
auto prefix_fuzz = max(fuzz + patch_prefix_context - context, 0);
// If the fuzz is greater than the total number of lines for a hunk, then it may be possible for the hunk to match anything.
if (suffix_fuzz + prefix_fuzz >= hunk.lines.size())
return {};
auto hunk_matches_starting_from_line = [&](size_t line) {
line += prefix_fuzz;
// Ensure that all of the lines in the hunk match starting from 'line', ignoring the specified number of context lines.
return all_of(hunk.lines.begin() + prefix_fuzz, hunk.lines.end() - suffix_fuzz, [&](const Line& hunk_line) {
// Ignore additions in our increment of line and comparison as they are not part of the 'original file'
if (hunk_line.operation == Line::Operation::Addition)
return true;
if (line >= content.size())
return false;
if (content[line] != hunk_line.content)
return false;
++line;
return true;
});
};
for (size_t line = offset_guess; line < content.size(); ++line) {
if (hunk_matches_starting_from_line(line))
return Location { line, fuzz, static_cast<ssize_t>(line - offset_guess) };
}
for (size_t line = offset_guess; line != 0; --line) {
if (hunk_matches_starting_from_line(line - 1))
return Location { line - 1, fuzz, static_cast<ssize_t>(line - offset_guess) };
}
}
// No bueno.
return {};
}
static ErrorOr<size_t> write_hunk(Stream& out, Hunk const& hunk, Location const& location, Vector<StringView> const& lines)
{
auto line_number = location.line_number;
for (auto const& patch_line : hunk.lines) {
if (patch_line.operation == Line::Operation::Context) {
TRY(out.write_formatted("{}\n", lines.at(line_number)));
++line_number;
} else if (patch_line.operation == Line::Operation::Addition) {
TRY(out.write_formatted("{}\n", patch_line.content));
} else if (patch_line.operation == Line::Operation::Removal) {
++line_number;
}
}
return line_number;
}
ErrorOr<void> apply_patch(Stream& out, Vector<StringView> const& lines, Patch const& patch)
{
size_t line_number = 0; // NOTE: relative to 'old' file.
ssize_t offset_error = 0;
for (size_t hunk_num = 0; hunk_num < patch.hunks.size(); ++hunk_num) {
auto const& hunk = patch.hunks[hunk_num];
auto maybe_location = locate_hunk(lines, hunk, offset_error);
if (!maybe_location.has_value())
return Error::from_string_literal("Failed to locate where to apply patch");
auto location = *maybe_location;
offset_error += location.offset;
// Write up until where we have found this latest hunk from the old file.
for (; line_number < location.line_number; ++line_number)
TRY(out.write_formatted("{}\n", lines.at(line_number)));
// Then output the hunk to what we hope is the correct location in the file.
line_number = TRY(write_hunk(out, hunk, location, lines));
}
// We've finished applying all hunks, write out anything from the old file we haven't already.
for (; line_number < lines.size(); ++line_number)
TRY(out.write_formatted("{}\n", lines[line_number]));
return {};
}
}