mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-18 05:17:55 +02:00
081598d989
Now we can stage lines by range and we can also stage reversals meaning we can delete lines or move lines from the working tree to the index and vice versa. I looked at how a few different git guis achieved this to iron out some edge cases, notably ungit and git cola. The end result is disstinct from both those repos, but I know people care about licensing and stuff so I'm happy to revisit this if somebody considers it derivative.
218 lines
5.7 KiB
Go
218 lines
5.7 KiB
Go
package git
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var headerRegexp = regexp.MustCompile("(?m)^@@ -(\\d+)[^\\+]+\\+(\\d+)[^@]+@@(.*)$")
|
|
|
|
type PatchHunk struct {
|
|
header string
|
|
FirstLineIdx int
|
|
LastLineIdx int
|
|
bodyLines []string
|
|
}
|
|
|
|
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
|
|
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
|
|
|
|
return &PatchHunk{
|
|
header: header,
|
|
FirstLineIdx: firstLineIdx,
|
|
LastLineIdx: firstLineIdx + len(bodyLines),
|
|
bodyLines: bodyLines,
|
|
}
|
|
}
|
|
|
|
func (hunk *PatchHunk) updatedLinesForRange(firstLineIdx int, lastLineIdx int, reverse bool) []string {
|
|
skippedNewlineMessageIndex := -1
|
|
newLines := []string{}
|
|
|
|
lineIdx := hunk.FirstLineIdx
|
|
for _, line := range hunk.bodyLines {
|
|
lineIdx++ // incrementing at the start to skip the header line
|
|
if line == "" {
|
|
break
|
|
}
|
|
isLineInsideRange := (firstLineIdx <= lineIdx && lineIdx <= lastLineIdx)
|
|
|
|
firstChar, content := line[:1], line[1:]
|
|
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineInsideRange)
|
|
|
|
if isLineInsideRange || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
|
|
newLines = append(newLines, transformedFirstChar+content)
|
|
continue
|
|
}
|
|
|
|
if transformedFirstChar == "+" {
|
|
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
|
|
skippedNewlineMessageIndex = lineIdx + 1
|
|
}
|
|
}
|
|
|
|
return newLines
|
|
}
|
|
|
|
func transformedFirstChar(firstChar string, reverse bool, isLineInsideRange bool) string {
|
|
if reverse {
|
|
if !isLineInsideRange && firstChar == "+" {
|
|
return " "
|
|
} else if firstChar == "-" {
|
|
return "+"
|
|
} else if firstChar == "+" {
|
|
return "-"
|
|
} else {
|
|
return firstChar
|
|
}
|
|
}
|
|
|
|
if !isLineInsideRange && firstChar == "-" {
|
|
return " "
|
|
}
|
|
|
|
return firstChar
|
|
}
|
|
|
|
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
|
|
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
|
|
}
|
|
|
|
func (hunk *PatchHunk) formatWithChanges(firstLineIdx int, lastLineIdx int, reverse bool, startOffset int) (int, string) {
|
|
bodyLines := hunk.updatedLinesForRange(firstLineIdx, lastLineIdx, reverse)
|
|
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
|
|
if !ok {
|
|
return startOffset, ""
|
|
}
|
|
return startOffset, header + strings.Join(bodyLines, "")
|
|
}
|
|
|
|
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
|
|
changeCount := 0
|
|
oldLength := 0
|
|
newLength := 0
|
|
for _, line := range newBodyLines {
|
|
switch line[:1] {
|
|
case "+":
|
|
newLength++
|
|
changeCount++
|
|
case "-":
|
|
oldLength++
|
|
changeCount++
|
|
case " ":
|
|
oldLength++
|
|
newLength++
|
|
}
|
|
}
|
|
|
|
if changeCount == 0 {
|
|
// if nothing has changed we just return nothing
|
|
return startOffset, "", false
|
|
}
|
|
|
|
// get oldstart, newstart, and heading from header
|
|
match := headerRegexp.FindStringSubmatch(hunk.header)
|
|
|
|
var oldStart int
|
|
if reverse {
|
|
oldStart = mustConvertToInt(match[2])
|
|
} else {
|
|
oldStart = mustConvertToInt(match[1])
|
|
}
|
|
heading := match[3]
|
|
|
|
var newStartOffset int
|
|
// if the hunk went from zero to positive length, we need to increment the starting point by one
|
|
// if the hunk went from positive to zero length, we need to decrement the starting point by one
|
|
if oldLength == 0 {
|
|
newStartOffset = 1
|
|
} else if newLength == 0 {
|
|
newStartOffset = -1
|
|
} else {
|
|
newStartOffset = 0
|
|
}
|
|
|
|
newStart := oldStart + startOffset + newStartOffset
|
|
|
|
newStartOffset = startOffset + newLength - oldLength
|
|
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, heading)
|
|
return newStartOffset, formattedHeader, true
|
|
}
|
|
|
|
func mustConvertToInt(s string) int {
|
|
i, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return i
|
|
}
|
|
|
|
func GetHunksFromDiff(diff string) []*PatchHunk {
|
|
headers := headerRegexp.FindAllString(diff, -1)
|
|
bodies := headerRegexp.Split(diff, -1)[1:] // discarding top bit
|
|
|
|
headerFirstLineIndices := []int{}
|
|
for lineIdx, line := range strings.Split(diff, "\n") {
|
|
if strings.HasPrefix(line, "@@ -") {
|
|
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
|
|
}
|
|
}
|
|
|
|
hunks := make([]*PatchHunk, len(headers))
|
|
for index, header := range headers {
|
|
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
|
|
}
|
|
|
|
return hunks
|
|
}
|
|
|
|
type PatchModifier struct {
|
|
Log *logrus.Entry
|
|
filename string
|
|
hunks []*PatchHunk
|
|
}
|
|
|
|
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
|
|
return &PatchModifier{
|
|
Log: log,
|
|
filename: filename,
|
|
hunks: GetHunksFromDiff(diffText),
|
|
}
|
|
}
|
|
|
|
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool) string {
|
|
// step one is getting only those hunks which we care about
|
|
hunksInRange := []*PatchHunk{}
|
|
for _, hunk := range d.hunks {
|
|
if hunk.LastLineIdx >= firstLineIdx && hunk.FirstLineIdx <= lastLineIdx {
|
|
hunksInRange = append(hunksInRange, hunk)
|
|
}
|
|
}
|
|
|
|
// step 2 is collecting all the hunks with new headers
|
|
startOffset := 0
|
|
formattedHunks := ""
|
|
var formattedHunk string
|
|
for _, hunk := range hunksInRange {
|
|
startOffset, formattedHunk = hunk.formatWithChanges(firstLineIdx, lastLineIdx, reverse, startOffset)
|
|
formattedHunks += formattedHunk
|
|
}
|
|
|
|
if formattedHunks == "" {
|
|
return ""
|
|
}
|
|
|
|
fileHeader := fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
|
|
|
|
return fileHeader + formattedHunks
|
|
}
|
|
|
|
func ModifiedPatch(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool) string {
|
|
p := NewPatchModifier(log, filename, diffText)
|
|
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse)
|
|
}
|