| // Copyright 2017-2020 The Verible Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include "common/strings/patch.h" | |
| #include <deque> | |
| #include <iostream> | |
| #include <iterator> | |
| #include "absl/base/macros.h" | |
| #include "absl/status/status.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/numbers.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_split.h" | |
| #include "common/strings/position.h" | |
| #include "common/strings/split.h" | |
| #include "common/util/algorithm.h" | |
| #include "common/util/container_iterator_range.h" | |
| #include "common/util/file_util.h" | |
| #include "common/util/iterator_adaptors.h" | |
| #include "common/util/iterator_range.h" | |
| #include "common/util/user_interaction.h" | |
| namespace verible { | |
| static bool LineMarksOldFile(absl::string_view line) { | |
| return absl::StartsWith(line, "--- "); | |
| } | |
| static bool IsValidMarkedLine(absl::string_view line) { | |
| if (line.empty()) return false; | |
| switch (line.front()) { | |
| case ' ': | |
| case '-': | |
| case '+': | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| namespace internal { | |
| template <typename RangeT, typename Iter> | |
| static std::vector<RangeT> IteratorsToRanges(const std::vector<Iter>& iters) { | |
| // TODO(fangism): This pattern appears elsewhere in the codebase, so refactor. | |
| CHECK_GE(iters.size(), 2); | |
| std::vector<RangeT> result; | |
| result.reserve(iters.size()); | |
| auto prev = iters.begin(); | |
| for (auto next = std::next(prev); next != iters.end(); prev = next, ++next) { | |
| result.emplace_back(*prev, *next); | |
| } | |
| return result; | |
| } | |
| absl::Status MarkedLine::Parse(absl::string_view text) { | |
| // text is already a whole line | |
| if (!IsValidMarkedLine(text)) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "MarkedLine must begin with one of [ -+], but got: \"", text, "\".")); | |
| } | |
| line.assign(text.begin(), text.end()); // copy | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const MarkedLine& line) { | |
| return stream << line.line; | |
| } | |
| absl::Status HunkIndices::Parse(absl::string_view text) { | |
| // text is expected to look like "int,int" | |
| StringSpliterator splitter(text); | |
| const absl::string_view start_text = splitter(','); | |
| const absl::string_view count_text = splitter(','); | |
| if (!absl::SimpleAtoi(start_text, &start) || // | |
| !absl::SimpleAtoi(count_text, &count) || // | |
| splitter /* unexpected second ',' */) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("HunkIndices expects int,int, but got: ", text, "\".")); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const HunkIndices& indices) { | |
| return stream << indices.start << ',' << indices.count; | |
| } | |
| absl::Status HunkHeader::Parse(absl::string_view text) { | |
| constexpr absl::string_view kDelimiter("@@"); | |
| StringSpliterator tokenizer(text); | |
| { | |
| absl::string_view first = tokenizer(kDelimiter); | |
| // first token should be empty | |
| if (!first.empty() || !tokenizer) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "HunkHeader should start with @@, but got: ", text, "\".")); | |
| } | |
| } | |
| // Parse ranges between the "@@"s. | |
| { | |
| const absl::string_view ranges = | |
| absl::StripAsciiWhitespace(tokenizer(kDelimiter)); | |
| if (!tokenizer) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "HunkHeader expects ranges in @@...@@, but got: ", text, "\".")); | |
| } | |
| auto splitter = MakeStringSpliterator(ranges, ' '); | |
| absl::string_view old_range_str(splitter()); | |
| if (!absl::ConsumePrefix(&old_range_str, "-")) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "old-file range should start with '-', but got: ", old_range_str, | |
| "\".")); | |
| } | |
| absl::string_view new_range_str(splitter()); | |
| if (!absl::ConsumePrefix(&new_range_str, "+")) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "new-file range should start with '+', but got: ", new_range_str, | |
| "\".")); | |
| } | |
| if (auto status = old_range.Parse(old_range_str); !status.ok()) { | |
| return status; | |
| } | |
| if (auto status = new_range.Parse(new_range_str); !status.ok()) { | |
| return status; | |
| } | |
| } | |
| // Text that follows the last "@@" provides context and is optional. | |
| const absl::string_view trailing_text = tokenizer(kDelimiter); | |
| context.assign(trailing_text.begin(), trailing_text.end()); | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const HunkHeader& header) { | |
| return stream << "@@ -" << header.old_range << " +" << header.new_range | |
| << " @@" << header.context; | |
| } | |
| // Type M could be any container or range of MarkedLines. | |
| template <class M> | |
| static void CountMarkedLines(const M& lines, int* before, int* after) { | |
| *before = 0; | |
| *after = 0; | |
| for (const MarkedLine& line : lines) { | |
| switch (line.Marker()) { | |
| case ' ': // line is common to both, unchanged | |
| ++*before; | |
| ++*after; | |
| break; | |
| case '-': | |
| ++*before; | |
| break; | |
| case '+': | |
| ++*after; | |
| break; | |
| } | |
| } | |
| } | |
| absl::Status Hunk::IsValid() const { | |
| int original_lines = 0; | |
| int new_lines = 0; | |
| CountMarkedLines(lines_, &original_lines, &new_lines); | |
| if (original_lines != header_.old_range.count) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Hunk is invalid: expected ", header_.old_range.count, | |
| " lines before, but got ", original_lines, ".")); | |
| } | |
| if (new_lines != header_.new_range.count) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Hunk is invalid: expected ", header_.new_range.count, | |
| " lines after, but got ", new_lines, ".")); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| void Hunk::UpdateHeader() { | |
| CountMarkedLines(lines_, &header_.old_range.count, &header_.new_range.count); | |
| } | |
| LineNumberSet Hunk::AddedLines() const { | |
| LineNumberSet line_numbers; | |
| int line_number = header_.new_range.start; | |
| for (const MarkedLine& line : lines_) { | |
| if (line.IsAdded()) line_numbers.Add(line_number); | |
| if (!line.IsDeleted()) ++line_number; | |
| } | |
| return line_numbers; | |
| } | |
| absl::Status Hunk::VerifyAgainstOriginalLines( | |
| const std::vector<absl::string_view>& original_lines) const { | |
| int line_number = header_.old_range.start; // 1-indexed | |
| for (const MarkedLine& line : lines_) { | |
| if (line.IsAdded()) continue; // ignore added lines | |
| if (line_number > static_cast<int>(original_lines.size())) { | |
| return absl::OutOfRangeError(absl::StrCat( | |
| "Patch hunk references line ", line_number, " in a file with only ", | |
| original_lines.size(), " lines")); | |
| } | |
| const absl::string_view original_line = original_lines[line_number - 1]; | |
| if (line.Text() != original_line) { | |
| return absl::DataLossError(absl::StrCat( | |
| "Patch is inconsistent with original file!\nHunk at line ", | |
| line_number, " expected:\n", line.Text(), "\nbut got (original):\n", | |
| original_line, "\n")); | |
| } | |
| ++line_number; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| class HunkSplitter { | |
| public: | |
| HunkSplitter() = default; | |
| bool operator()(const MarkedLine& line) { | |
| if (previous_line_ == nullptr) { | |
| // first line | |
| previous_line_ = &line; | |
| return true; | |
| } | |
| const bool new_hunk = !previous_line_->IsCommon() && line.IsCommon(); | |
| previous_line_ = &line; | |
| return new_hunk; | |
| } | |
| private: | |
| const MarkedLine* previous_line_ = nullptr; | |
| }; | |
| std::vector<Hunk> Hunk::Split() const { | |
| std::vector<Hunk> sub_hunks; | |
| // Split after every group of changed lines. | |
| typedef std::vector<MarkedLine>::const_iterator MarkedLineIterator; | |
| std::vector<MarkedLineIterator> cut_points; | |
| verible::find_all(lines_.begin(), lines_.end(), | |
| std::back_inserter(cut_points), HunkSplitter()); | |
| CHECK(!cut_points.empty()); | |
| // Check whether or not there any line changes after the last cut point. | |
| // If not, then delete that cut point, which merges the last two partitions. | |
| if (cut_points.back() != lines_.begin()) { | |
| const bool last_partition_has_any_changes = | |
| std::any_of(cut_points.back(), lines_.end(), | |
| [](const MarkedLine& m) { return !m.IsCommon(); }); | |
| if (!last_partition_has_any_changes) cut_points.pop_back(); | |
| } | |
| cut_points.push_back(lines_.end()); // always terminate partitions with end() | |
| // cut_points always contains lines_.begin(), and .end() | |
| // convert cut points to sub-ranges | |
| typedef container_iterator_range<MarkedLineIterator> MarkedLineRange; | |
| const std::vector<MarkedLineRange> ranges( | |
| IteratorsToRanges<MarkedLineRange>(cut_points)); | |
| // create sub hunks from each sub-range | |
| int old_starting_line = header_.old_range.start; | |
| int new_starting_line = header_.new_range.start; | |
| for (const MarkedLineRange& marked_line_range : ranges) { | |
| sub_hunks.emplace_back(old_starting_line, new_starting_line, | |
| marked_line_range.begin(), marked_line_range.end()); | |
| const HunkHeader& recent_header(sub_hunks.back().Header()); | |
| old_starting_line += recent_header.old_range.count; | |
| new_starting_line += recent_header.new_range.count; | |
| } | |
| // TODO(b/161416776): if the resulting sub_hunks is singleton, then attempt to | |
| // further subdivide line-by-line. | |
| return sub_hunks; | |
| } | |
| absl::Status Hunk::Parse(const LineRange& hunk_lines) { | |
| if (auto status = header_.Parse(hunk_lines.front()); !status.ok()) { | |
| return status; | |
| } | |
| LineRange body(hunk_lines); | |
| body.pop_front(); // remove the header | |
| lines_.resize(body.size()); | |
| auto line_iter = lines_.begin(); | |
| for (const auto& line : body) { | |
| if (auto status = line_iter->Parse(line); !status.ok()) { | |
| return status; | |
| } | |
| ++line_iter; | |
| } | |
| return IsValid(); | |
| } | |
| std::ostream& Hunk::Print(std::ostream& stream) const { | |
| stream << header_ << std::endl; | |
| for (const MarkedLine& line : lines_) { | |
| stream << line << std::endl; | |
| } | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const Hunk& hunk) { | |
| return hunk.Print(stream); | |
| } | |
| absl::Status SourceInfo::Parse(absl::string_view text) { | |
| StringSpliterator splitter(text); | |
| absl::string_view token = splitter('\t'); | |
| path.assign(token.begin(), token.end()); | |
| if (path.empty()) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "Expected \"path [timestamp]\" (tab-separated), but got: \"", text, | |
| "\".")); | |
| } | |
| // timestamp is optional, allowed to be empty | |
| token = splitter('\t'); // time string (optional) is not parsed any further | |
| timestamp.assign(token.begin(), token.end()); | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const SourceInfo& info) { | |
| stream << info.path; | |
| if (!info.timestamp.empty()) stream << '\t' << info.timestamp; | |
| return stream; | |
| } | |
| static absl::Status ParseSourceInfoWithMarker( | |
| SourceInfo* info, absl::string_view line, | |
| absl::string_view expected_marker) { | |
| StringSpliterator splitter(line); | |
| absl::string_view marker = splitter(' '); | |
| if (marker != expected_marker) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Expected old-file marker \"", expected_marker, | |
| "\", but got: \"", marker, "\"")); | |
| } | |
| return info->Parse(splitter.Remainder()); | |
| } | |
| bool FilePatch::IsNewFile() const { return old_file_.path == "/dev/null"; } | |
| bool FilePatch::IsDeletedFile() const { return new_file_.path == "/dev/null"; } | |
| LineNumberSet FilePatch::AddedLines() const { | |
| LineNumberSet line_numbers; | |
| for (const Hunk& hunk : hunks_) { | |
| line_numbers.Union(hunk.AddedLines()); | |
| } | |
| return line_numbers; | |
| } | |
| static char PromptHunkAction(std::istream& ins, std::ostream& outs) { | |
| // Suppress prompt in noninteractive mode. | |
| if (IsInteractiveTerminalSession()) { | |
| outs << "Apply this hunk? [y,n,a,d,s,q,?] "; | |
| } | |
| char c; | |
| ins >> c; // user will need to hit <enter> after the character | |
| if (ins.eof()) { | |
| outs << "Reached end of user input, abandoning changes to this file and " | |
| "all remaining files." | |
| << std::endl; | |
| return 'q'; | |
| } | |
| return c; | |
| } | |
| absl::Status FilePatch::VerifyAgainstOriginalLines( | |
| const std::vector<absl::string_view>& original_lines) const { | |
| for (const Hunk& hunk : hunks_) { | |
| if (auto status = hunk.VerifyAgainstOriginalLines(original_lines); | |
| !status.ok()) { | |
| return status; | |
| } | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status FilePatch::PickApplyInPlace(std::istream& ins, | |
| std::ostream& outs) const { | |
| return PickApply(ins, outs, &file::GetContents, &file::SetContents); | |
| } | |
| absl::Status FilePatch::PickApply(std::istream& ins, std::ostream& outs, | |
| const FileReaderFunction& file_reader, | |
| const FileWriterFunction& file_writer) const { | |
| if (IsDeletedFile()) return absl::OkStatus(); // ignore | |
| if (IsNewFile()) return absl::OkStatus(); // ignore | |
| // Since this structure represents a patch, we need to retrieve the original | |
| // file's contents in whole. | |
| // If we had control over diff/patch generation, then we could rely on the | |
| // original diff structure/stream to provide original contents. | |
| // Below, we VerifyAgainstOriginalLines for all hunks in this FilePatch. | |
| std::string original_file; | |
| if (auto status = file_reader(old_file_.path, &original_file); !status.ok()) { | |
| return status; | |
| } | |
| if (!hunks_.empty()) { | |
| // Display the file being processed, if there are any hunks. | |
| outs << "--- " << old_file_.path << std::endl; | |
| outs << "+++ " << new_file_.path << std::endl; | |
| } | |
| const std::vector<absl::string_view> orig_lines(SplitLines(original_file)); | |
| if (auto status = VerifyAgainstOriginalLines(orig_lines); !status.ok()) { | |
| return status; | |
| } | |
| // Accumulate lines to write here. | |
| // Needs own copy of string due to potential splitting into temporary hunks. | |
| std::vector<std::string> output_lines; | |
| int last_consumed_line = 0; // 0-indexed | |
| std::deque<Hunk> hunks_worklist(hunks_.begin(), hunks_.end()); // copy-fill | |
| while (!hunks_worklist.empty()) { | |
| VLOG(1) << "hunks remaining: " << hunks_worklist.size(); | |
| const Hunk& hunk(hunks_worklist.front()); | |
| // Copy over unchanged lines before this hunk. | |
| const auto& old_range = hunk.Header().old_range; | |
| if (old_range.start < last_consumed_line) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Hunks are not properly ordered. last_consumed_line=", | |
| last_consumed_line, ", but current hunk starts at line ", | |
| old_range.start)); | |
| } | |
| for (; last_consumed_line < old_range.start - 1; ++last_consumed_line) { | |
| CHECK_LT(last_consumed_line, static_cast<int>(orig_lines.size())); | |
| const absl::string_view& line(orig_lines[last_consumed_line]); | |
| output_lines.emplace_back(line.begin(), line.end()); // copy string | |
| } | |
| VLOG(1) << "copied up to (!including) line[" << last_consumed_line << "]."; | |
| // Prompt user to apply or reject patch hunk. | |
| std::function<char()> prompt = [&hunk, &ins, &outs]() -> char { | |
| outs << hunk; | |
| return PromptHunkAction(ins, outs); | |
| }; | |
| const char action = prompt(); | |
| VLOG(1) << "user entered: " << action; | |
| switch (action) { | |
| case 'a': { | |
| // accept all remaining hunks in the current file | |
| prompt = []() -> char { return 'y'; }; // suppress prompt | |
| ABSL_FALLTHROUGH_INTENDED; | |
| } | |
| case 'y': { | |
| // accept this hunk, copy lines over | |
| for (const MarkedLine& marked_line : hunk.MarkedLines()) { | |
| if (!marked_line.IsDeleted()) { | |
| const absl::string_view line(marked_line.Text()); | |
| output_lines.emplace_back(line.begin(), line.end()); // copy string | |
| } | |
| } | |
| last_consumed_line = old_range.start + old_range.count - 1; | |
| break; // switch | |
| } | |
| case 'd': { | |
| // reject all remaining hunks in the current file | |
| prompt = []() -> char { return 'n'; }; // suppress prompt | |
| ABSL_FALLTHROUGH_INTENDED; | |
| } | |
| case 'n': | |
| // reject this hunk | |
| // no need to do anything, next iteration will sweep up original lines | |
| break; // switch | |
| case 's': { | |
| // split this hunk into smaller ones and prompt again | |
| const auto sub_hunks = hunk.Split(); | |
| hunks_worklist.pop_front(); // replace current hunk with sub-hunks | |
| // maintain sequential order of sub-hunks in the queue by line number | |
| for (const auto& sub_hunk : verible::reversed_view(sub_hunks)) { | |
| hunks_worklist.push_front(sub_hunk); | |
| } | |
| continue; // for-loop | |
| } | |
| // TODO(b/156530527): 'e' for hunk editing | |
| case 'q': | |
| // Abort this file, discard any elected edits. | |
| outs << "Leaving file " << old_file_.path << " unchanged." << std::endl; | |
| return absl::OkStatus(); | |
| default: // including '?' | |
| outs << "y - accept change\n" | |
| "n - reject change\n" | |
| "a - accept this and remaining changes in the current file\n" | |
| "d - reject this and remaining changes in the current file\n" | |
| "s - split this hunk into smaller ones and prompt each one\n" | |
| "q - abandon all changes in this file\n" | |
| "? - print this help and prompt again\n"; | |
| continue; // for-loop | |
| } | |
| hunks_worklist.pop_front(); | |
| } | |
| // Copy over remaining lines after the last hunk. | |
| for (; last_consumed_line < static_cast<int>(orig_lines.size()); | |
| ++last_consumed_line) { | |
| const absl::string_view& line(orig_lines[last_consumed_line]); | |
| output_lines.emplace_back(line.begin(), line.end()); // copy string | |
| } | |
| VLOG(1) << "copied reamining lines up to [" << last_consumed_line << "]."; | |
| const std::string rewrite_contents(absl::StrJoin(output_lines, "\n") + "\n"); | |
| return file_writer(old_file_.path, rewrite_contents); | |
| } | |
| absl::Status FilePatch::Parse(const LineRange& lines) { | |
| LineIterator line_iter( | |
| std::find_if(lines.begin(), lines.end(), &LineMarksOldFile)); | |
| if (lines.begin() == lines.end() || line_iter == lines.end()) { | |
| return absl::InvalidArgumentError( | |
| "Expected a file marker starting with \"---\", but did not find one."); | |
| } | |
| // Lines leading up to the old file marker "---" are metadata. | |
| for (const auto& line : make_range(lines.begin(), line_iter)) { | |
| metadata_.emplace_back(line); | |
| } | |
| if (auto status = ParseSourceInfoWithMarker(&old_file_, *line_iter, "---"); | |
| !status.ok()) { | |
| return status; | |
| } | |
| ++line_iter; | |
| if (line_iter == lines.end()) { | |
| return absl::InvalidArgumentError( | |
| "Expected a file marker starting with \"+++\", but did not find one."); | |
| } else { | |
| if (auto status = ParseSourceInfoWithMarker(&new_file_, *line_iter, "+++"); | |
| !status.ok()) { | |
| return status; | |
| } | |
| } | |
| ++line_iter; | |
| // find hunk starts, and parse ranges of hunk texts | |
| std::vector<LineIterator> hunk_starts; | |
| find_all( | |
| line_iter, lines.end(), std::back_inserter(hunk_starts), | |
| [](absl::string_view line) { return absl::StartsWith(line, "@@ "); }); | |
| if (hunk_starts.empty()) { | |
| // Unusual, but degenerate case of no hunks is parseable and valid. | |
| return absl::OkStatus(); | |
| } | |
| hunk_starts.push_back(lines.end()); // make it easier to construct ranges | |
| const std::vector<LineRange> hunk_ranges( | |
| IteratorsToRanges<LineRange>(hunk_starts)); | |
| hunks_.resize(hunk_ranges.size()); | |
| auto hunk_iter = hunks_.begin(); | |
| for (const auto& hunk_range : hunk_ranges) { | |
| if (auto status = hunk_iter->Parse(hunk_range); !status.ok()) { | |
| return status; | |
| } | |
| ++hunk_iter; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& FilePatch::Print(std::ostream& stream) const { | |
| for (const std::string& line : metadata_) { | |
| stream << line << std::endl; | |
| } | |
| stream << "--- " << old_file_ << '\n' // | |
| << "+++ " << new_file_ << std::endl; | |
| for (const Hunk& hunk : hunks_) { | |
| stream << hunk; | |
| } | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const FilePatch& patch) { | |
| return patch.Print(stream); | |
| } | |
| } // namespace internal | |
| static bool LineBelongsToPreviousSection(absl::string_view line) { | |
| if (line.empty()) return true; | |
| return IsValidMarkedLine(line); | |
| } | |
| absl::Status PatchSet::Parse(absl::string_view patch_contents) { | |
| // Split lines. The resulting lines will not include the \n delimiters. | |
| std::vector<absl::string_view> lines( | |
| absl::StrSplit(patch_contents, absl::ByChar('\n'))); | |
| // Consider an empty patch file valid. | |
| if (lines.empty()) return absl::OkStatus(); | |
| // Well-formed files end with a newline [POSIX], so delete the last partition. | |
| internal::LineRange lines_range(lines.cbegin(), std::prev(lines.cend())); | |
| // Split set of lines into ranges that correspond to individual files. | |
| // Strategy: find all old-file lines that start with "--- ", and then | |
| // search backwards to find the last line that starts with one of [ +-]. | |
| std::vector<internal::LineIterator> file_patch_begins; | |
| { | |
| find_all(lines_range.begin(), lines_range.end(), | |
| std::back_inserter(file_patch_begins), &LineMarksOldFile); | |
| if (file_patch_begins.empty()) return absl::OkStatus(); | |
| // Move line iterators back to correct starting points. | |
| for (auto& iter : file_patch_begins) { | |
| while (iter != lines_range.begin()) { | |
| const auto prev = std::prev(iter); | |
| const absl::string_view& peek(*prev); | |
| if (LineBelongsToPreviousSection(peek)) break; | |
| iter = prev; | |
| } | |
| } | |
| // For easier construction of ranges, append an end() iterator. | |
| file_patch_begins.push_back(lines_range.end()); | |
| } | |
| // Record metadata lines, if there are any. | |
| for (const auto& line : | |
| make_range(lines_range.begin(), file_patch_begins.front())) { | |
| metadata_.emplace_back(line); | |
| } | |
| // Parse individual file patches. | |
| const std::vector<internal::LineRange> file_patch_ranges( | |
| internal::IteratorsToRanges<internal::LineRange>(file_patch_begins)); | |
| file_patches_.resize(file_patch_ranges.size()); | |
| auto iter = file_patches_.begin(); | |
| for (const auto& range : file_patch_ranges) { | |
| if (auto status = iter->Parse(range); !status.ok()) { | |
| return status; | |
| } | |
| ++iter; | |
| } | |
| // TODO(fangism): pass around line numbers to include in diagnostics | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& PatchSet::Render(std::ostream& stream) const { | |
| for (const auto& line : metadata_) { | |
| stream << line << std::endl; | |
| } | |
| for (const internal::FilePatch& file_patch : file_patches_) { | |
| stream << file_patch; | |
| } | |
| return stream; | |
| } | |
| FileLineNumbersMap PatchSet::AddedLinesMap(bool new_file_ranges) const { | |
| FileLineNumbersMap result; | |
| for (const internal::FilePatch& file_patch : file_patches_) { | |
| if (file_patch.IsDeletedFile()) continue; | |
| LineNumberSet& entry = result[file_patch.NewFileInfo().path]; | |
| if (file_patch.IsNewFile() && !new_file_ranges) { | |
| entry.clear(); | |
| } else { | |
| entry = file_patch.AddedLines(); | |
| } | |
| } | |
| return result; | |
| } | |
| absl::Status PatchSet::PickApplyInPlace(std::istream& ins, | |
| std::ostream& outs) const { | |
| return PickApply(ins, outs, &file::GetContents, &file::SetContents); | |
| } | |
| absl::Status PatchSet::PickApply( | |
| std::istream& ins, std::ostream& outs, | |
| const internal::FileReaderFunction& file_reader, | |
| const internal::FileWriterFunction& file_writer) const { | |
| for (const internal::FilePatch& file_patch : file_patches_) { | |
| if (auto status = file_patch.PickApply(ins, outs, file_reader, file_writer); | |
| !status.ok()) { | |
| return status; | |
| } | |
| } | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const PatchSet& patch) { | |
| return patch.Render(stream); | |
| } | |
| } // namespace verible |