Files

258 lines
8.1 KiB
Go

package patch
import (
"github.com/samber/lo"
)
type Patch struct {
// header of the patch (split on newlines) e.g.
// diff --git a/filename b/filename
// index dcd3485..1ba5540 100644
// --- a/filename
// +++ b/filename
header []string
// hunks of the patch
hunks []*Hunk
}
// Returns a new patch with the specified transformation applied (e.g.
// only selecting a subset of changes).
// Leaves the original patch unchanged.
func (self *Patch) Transform(opts TransformOpts) *Patch {
return transform(self, opts)
}
// Returns the patch as a plain string
func (self *Patch) FormatPlain() string {
return formatPlain(self)
}
// Returns a range of lines from the patch as a plain string (range is inclusive)
func (self *Patch) FormatRangePlain(startIdx int, endIdx int) string {
return formatRangePlain(self, startIdx, endIdx)
}
// Returns the patch as a string with ANSI color codes for displaying in a view
func (self *Patch) FormatView(opts FormatViewOpts) string {
return formatView(self, opts)
}
// Returns the lines of the patch
func (self *Patch) Lines() []*PatchLine {
lines := []*PatchLine{}
for _, line := range self.header {
lines = append(lines, &PatchLine{Content: line, Kind: PATCH_HEADER})
}
for _, hunk := range self.hunks {
lines = append(lines, hunk.allLines()...)
}
return lines
}
// Returns the old-file starting line number of the hunk containing the given
// patch line index. Returns 0 if the line is not inside any hunk.
func (self *Patch) HunkOldStartForLine(idx int) int {
hunkIdx := self.HunkContainingLine(idx)
if hunkIdx == -1 {
return 0
}
return self.hunks[hunkIdx].oldStart
}
// Returns the patch line index of the first line in the given hunk
func (self *Patch) HunkStartIdx(hunkIndex int) int {
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
result := len(self.header)
for i := range hunkIndex {
result += self.hunks[i].lineCount()
}
return result
}
// Returns the patch line index of the last line in the given hunk
func (self *Patch) HunkEndIdx(hunkIndex int) int {
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
}
// IsWellFormed reports whether every hunk's body matches the lengths declared in
// its header. A faithful unified diff always satisfies this; a rendering that
// restructured the diff body does not — a diff renderer that puts line numbers in
// a gutter, say, shifts the +/- marker off the start of each line, so every body
// line reads as context and the computed lengths no longer match the header. That
// makes this the test for whether a rendered diff can be parsed as a unified diff
// at all, rather than trusting a mis-parse. Only meaningful for patches produced
// by Parse, which is where the declared lengths come from.
func (self *Patch) IsWellFormed() bool {
return lo.NoneBy(self.hunks, func(hunk *Hunk) bool {
return hunk.oldLength() != hunk.declaredOldLength || hunk.newLength() != hunk.declaredNewLength
})
}
func (self *Patch) ContainsChanges() bool {
return lo.SomeBy(self.hunks, func(hunk *Hunk) bool {
return hunk.containsChanges()
})
}
// Takes a line index in the patch and returns the line number in the new file.
// If the line is a header line, returns 1.
// If the line is a hunk header line, returns the first file line number in that hunk.
// If the line is out of range below, returns the last file line number in the last hunk.
func (self *Patch) LineNumberOfLine(idx int) int {
if idx < len(self.header) || len(self.hunks) == 0 {
return 1
}
hunkIdx := self.HunkContainingLine(idx)
// cursor out of range, return last file line number
if hunkIdx == -1 {
lastHunk := self.hunks[len(self.hunks)-1]
return lastHunk.newStart + lastHunk.newLength() - 1
}
hunk := self.hunks[hunkIdx]
hunkStartIdx := self.HunkStartIdx(hunkIdx)
idxInHunk := idx - hunkStartIdx
if idxInHunk == 0 {
return hunk.newStart
}
lines := hunk.bodyLines[:idxInHunk-1]
offset := nLinesWithKind(lines, []PatchLineKind{ADDITION, CONTEXT})
return hunk.newStart + offset
}
// Takes a line index in the patch and returns the line number in the old file.
// This is the old-file counterpart of LineNumberOfLine; for a deletion it gives
// the line's position in the old file (additions get the position they sit at).
// If the line is a header line, returns 1.
// If the line is a hunk header line, returns the first old-file line number in that hunk.
// If the line is out of range below, returns the last old-file line number in the last hunk.
func (self *Patch) OldLineNumberOfLine(idx int) int {
if idx < len(self.header) || len(self.hunks) == 0 {
return 1
}
hunkIdx := self.HunkContainingLine(idx)
// cursor out of range, return last file line number
if hunkIdx == -1 {
lastHunk := self.hunks[len(self.hunks)-1]
return lastHunk.oldStart + lastHunk.oldLength() - 1
}
hunk := self.hunks[hunkIdx]
hunkStartIdx := self.HunkStartIdx(hunkIdx)
idxInHunk := idx - hunkStartIdx
if idxInHunk == 0 {
return hunk.oldStart
}
lines := hunk.bodyLines[:idxInHunk-1]
offset := nLinesWithKind(lines, []PatchLineKind{DELETION, CONTEXT})
return hunk.oldStart + offset
}
// Returns hunk index containing the line at the given patch line index
func (self *Patch) HunkContainingLine(idx int) int {
for hunkIdx, hunk := range self.hunks {
hunkStartIdx := self.HunkStartIdx(hunkIdx)
if idx >= hunkStartIdx && idx < hunkStartIdx+hunk.lineCount() {
return hunkIdx
}
}
return -1
}
// Returns the patch line index of the next change (i.e. addition or deletion)
// that matches the same "included" state, given the includedLines. If you don't
// care about included states, pass nil for includedLines and false for included.
func (self *Patch) GetNextChangeIdxOfSameIncludedState(idx int, includedLines []int, included bool) (int, bool) {
idx = lo.Clamp(idx, 0, self.LineCount()-1)
lines := self.Lines()
isMatch := func(i int, line *PatchLine) bool {
sameIncludedState := lo.Contains(includedLines, i) == included
return line.IsChange() && sameIncludedState
}
for i, line := range lines[idx:] {
if isMatch(i+idx, line) {
return i + idx, true
}
}
// there are no changes from the cursor onwards so we'll instead
// return the index of the last change
for i := len(lines) - 1; i >= 0; i-- {
line := lines[i]
if isMatch(i, line) {
return i, true
}
}
return 0, false
}
// Returns the patch line index of the next change (i.e. addition or deletion).
func (self *Patch) GetNextChangeIdx(idx int) int {
result, _ := self.GetNextChangeIdxOfSameIncludedState(idx, nil, false)
return result
}
// Returns the length of the patch in lines
func (self *Patch) LineCount() int {
count := len(self.header)
for _, hunk := range self.hunks {
count += hunk.lineCount()
}
return count
}
// Returns the number of hunks of the patch
func (self *Patch) HunkCount() int {
return len(self.hunks)
}
// Adjust the given line number (one-based) according to the current patch. The
// patch is supposed to be a diff of an old file state against the working
// directory; the line number is a line number in that old file, and the
// function returns the corresponding line number in the working directory file.
func (self *Patch) AdjustLineNumber(lineNumber int) int {
adjustedLineNumber := lineNumber
for _, hunk := range self.hunks {
if hunk.oldStart >= lineNumber {
break
}
if hunk.oldStart+hunk.oldLength() > lineNumber {
return hunk.newStart
}
adjustedLineNumber += hunk.newLength() - hunk.oldLength()
}
return adjustedLineNumber
}
func (self *Patch) IsSingleHunkForWholeFile() bool {
if len(self.hunks) != 1 {
return false
}
// We consider a patch to be a single hunk for the whole file if it has only additions or
// deletions but not both, and no context lines. This not quite correct, because it will also
// return true for a block of added or deleted lines if the diff context size is 0, but in this
// case you wouldn't be able to stage things anyway, so it doesn't matter.
bodyLines := self.hunks[0].bodyLines
return nLinesWithKind(bodyLines, []PatchLineKind{DELETION, CONTEXT}) == 0 ||
nLinesWithKind(bodyLines, []PatchLineKind{ADDITION, CONTEXT}) == 0
}