blob: 26399efb9515ba73d03aa29640aa43f9001ba1a8 [file] [log] [blame]
// 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.
#ifndef VERIBLE_COMMON_STRINGS_PATCH_H_
#define VERIBLE_COMMON_STRINGS_PATCH_H_
#include <functional>
#include <iosfwd>
#include <map>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
#include "common/strings/compare.h"
#include "common/strings/position.h"
#include "common/util/container_iterator_range.h"
#include "common/util/logging.h"
namespace verible {
namespace internal {
// Forward declarations
class FilePatch;
// function interface like file::GetContents()
using FileReaderFunction = std::function<absl::Status(
absl::string_view filename, std::string* contents)>;
// function interface like file::SetContents()
using FileWriterFunction = std::function<absl::Status(
absl::string_view filename, absl::string_view contents)>;
} // namespace internal
using FileLineNumbersMap =
std::map<std::string, LineNumberSet, StringViewCompare>;
// Collection of file changes.
// Recursively inside these structs, we chose to copy std::string (owned memory)
// instead of string_views into file contents memory kept separately.
// This lets one safely modify patch structures.
class PatchSet {
public:
PatchSet() = default;
// Parse a unified-diff patch file into internal representation.
absl::Status Parse(absl::string_view patch_contents);
// Prints a unified-diff formatted output.
std::ostream& Render(std::ostream& stream) const;
// Returns a map<filename, line_numbers> that indicates which lines in each
// file are new (in patch files, these are hunk lines starting with '+').
// New and existing files will always have entries in the returned map,
// while deleted files will not.
// If `new_file_ranges` is true, provide the full range of lines for new
// files, otherwise leave their corresponding LineNumberSets empty.
FileLineNumbersMap AddedLinesMap(bool new_file_ranges) const;
// Interactively prompt user to select hunks to apply in-place.
// 'ins' is the stream from which user-input is read,
// and 'outs' is the stream that displays text and prompts to the user.
absl::Status PickApplyInPlace(std::istream& ins, std::ostream& outs) const;
protected:
// For testing, allow mocking out of file I/O.
absl::Status PickApply(std::istream& ins, std::ostream& outs,
const internal::FileReaderFunction& file_reader,
const internal::FileWriterFunction& file_writer) const;
private:
// Non-patch plain text that could describe the origins of the diff/patch,
// e.g. from git-format-patch.
std::vector<std::string> metadata_;
// Collection of file differences.
// Any metadata for the entire patch set will be lumped into the first file's
// metadata.
std::vector<internal::FilePatch> file_patches_;
};
std::ostream& operator<<(std::ostream&, const PatchSet&);
// Private implementation details follow.
namespace internal {
using LineIterator = std::vector<absl::string_view>::const_iterator;
using LineRange = container_iterator_range<LineIterator>;
// A single line of a patch hunk.
struct MarkedLine {
std::string line;
MarkedLine() = default;
// only used in manual test case construction
// so ok to use CHECK here.
explicit MarkedLine(absl::string_view text) : line(text.begin(), text.end()) {
CHECK(!line.empty()) << "MarkedLine must start with a marker in [ -+].";
CHECK(Valid()) << "Unexpected marker '" << Marker() << "'.";
}
bool Valid() const {
const char m = Marker();
return m == ' ' || m == '-' || m == '+';
}
// The first column denotes whether a line is:
// ' ': common context
// '-': only in the left/old file
// '+': only in the right/new file
char Marker() const { return line[0]; }
bool IsCommon() const { return Marker() == ' '; }
bool IsAdded() const { return Marker() == '+'; }
bool IsDeleted() const { return Marker() == '-'; }
absl::string_view Text() const {
return absl::string_view(&line[1], line.length() - 1);
}
// default equality operator
bool operator==(const MarkedLine& other) const { return line == other.line; }
bool operator!=(const MarkedLine& other) const { return !(*this == other); }
absl::Status Parse(absl::string_view);
};
std::ostream& operator<<(std::ostream&, const MarkedLine&);
struct HunkIndices {
int start; // starting line number for a hunk, 1-based
int count; // number of lines to expect in this hunk
// default equality operator
bool operator==(const HunkIndices& other) const {
return start == other.start && count == other.count;
}
bool operator!=(const HunkIndices& other) const { return !(*this == other); }
absl::Status Parse(absl::string_view);
};
std::ostream& operator<<(std::ostream&, const HunkIndices&);
struct HunkHeader {
HunkIndices old_range;
HunkIndices new_range;
// Some diff tools also include context such as the function or class
// declaration that encloses this hunk. This is optional metadata.
std::string context;
// default equality operator
bool operator==(const HunkHeader& other) const {
return old_range == other.old_range && new_range == other.new_range &&
context == other.context;
}
bool operator!=(const HunkHeader& other) const { return !(*this == other); }
absl::Status Parse(absl::string_view);
};
std::ostream& operator<<(std::ostream&, const HunkHeader&);
// One unit of a file change.
class Hunk {
public:
Hunk() = default;
Hunk(const Hunk&) = default;
template <typename Iter>
Hunk(int old_starting_line, int new_starting_line, Iter begin, Iter end)
: header_{.old_range = {old_starting_line, 0},
.new_range = {new_starting_line, 0}},
lines_(begin, end) /* copy */ {
UpdateHeader(); // automatically count marked lines
}
// Hunk is valid if its header's line counts are consistent with the set of
// MarkedLines.
absl::Status IsValid() const;
const HunkHeader& Header() const { return header_; }
const std::vector<MarkedLine>& MarkedLines() const { return lines_; }
// If a hunk is modified for any reason, the number of added/removed lines may
// have changed, so this will update the .count values.
void UpdateHeader();
// Returns a set of line numbers for lines that are changed or new.
LineNumberSet AddedLines() const;
absl::Status Parse(const LineRange&);
std::ostream& Print(std::ostream&) const;
// Verify consistency of lines in the patch (old-file) against the file that
// is read in whole.
absl::Status VerifyAgainstOriginalLines(
const std::vector<absl::string_view>& original_lines) const;
// default structural comparison
bool operator==(const Hunk& other) const {
return header_ == other.header_ && lines_ == other.lines_;
}
bool operator!=(const Hunk& other) const { return !(*this == other); }
// Splits this Hunk into smaller ones at the points of common context.
// Returns array of hunks. If this hunk can no longer be split, then it is
// returned as a singleton in the array.
std::vector<Hunk> Split() const;
private:
// The header describes how many of each type of edit lines to expect.
HunkHeader header_;
// Sequence of edit lines (common, old, new).
std::vector<MarkedLine> lines_;
};
std::ostream& operator<<(std::ostream&, const Hunk&);
struct SourceInfo {
std::string path; // location to patched file, absolute or relative
std::string timestamp; // unspecified date format, not parsed, optional
absl::Status Parse(absl::string_view);
};
std::ostream& operator<<(std::ostream&, const SourceInfo&);
// Set of changes for a single file.
class FilePatch {
public:
// Initialize data struct from a range of patch lines.
absl::Status Parse(const LineRange&);
// Returns true if this file is new.
bool IsNewFile() const;
// Returns true if this file is deleted.
bool IsDeletedFile() const;
const SourceInfo& NewFileInfo() const { return new_file_; }
// Returns a set of line numbers for lines that are changed or new.
LineNumberSet AddedLines() const;
std::ostream& Print(std::ostream&) const;
// Verify consistency of lines in the patch (old-file) against the file that
// is read in whole.
absl::Status VerifyAgainstOriginalLines(
const std::vector<absl::string_view>& original_lines) const;
absl::Status PickApplyInPlace(std::istream& ins, std::ostream& outs) const;
// For testing with mocked-out file I/O.
// Public to allow use by PatchSet::PickApply().
absl::Status PickApply(std::istream& ins, std::ostream& outs,
const FileReaderFunction& file_reader,
const FileWriterFunction& file_writer) const;
private:
// These are lines of informational text only, such as how the diff was
// generated. They do not impact 'patch' behavior.
std::vector<std::string> metadata_;
SourceInfo old_file_;
SourceInfo new_file_;
std::vector<Hunk> hunks_;
};
std::ostream& operator<<(std::ostream&, const FilePatch&);
} // namespace internal
} // namespace verible
#endif // VERIBLE_COMMON_STRINGS_PATCH_H_