/
githubmirror
/
lazygit
Обзор
Документация
Войти
/
githubmirror
/
lazygit
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
pkg/commands/patch/transform.go
274 строки
8 KB
Stefan Haller
Show renamed files in the custom patch builder
04 июл 2026, 14:05
04 июл 2026, 14:05
f84ada4
Код
Авторство
О чём код?
package patch import ( "strings" "github.com/samber/lo" ) type patchTransformer struct { patch *Patch opts TransformOpts } type TransformOpts struct { // Create a patch that will applied in reverse with `git apply --reverse`. // This affects how unselected lines are treated when only parts of a hunk // are selected: usually, for unselected lines we change '-' lines to // context lines and remove '+' lines, but when Reverse is true we need to // turn '+' lines into context lines and remove '-' lines. Reverse bool // If set, we will replace the original header with one referring to this file name. // For staging/unstaging lines we don't want the original header because // it makes git confused e.g. when dealing with deleted/added files // but with building and applying patches the original header gives git // information it needs to cleanly apply patches FileNameOverride string // Custom patches tend to work better when treating new files as diffs // against an empty file. The only case where we need this to be false is // when moving a custom patch to an earlier commit; in that case the patch // command would fail with the error "file does not exist in index" if we // treat it as a diff against an empty file. TurnAddedFilesIntoDiffAgainstEmptyFile bool // When building a partial patch for a renamed file, strip the rename // metadata from the header and point it at the new path. Applying the // resulting patch then only changes the file's contents and leaves the // rename itself in place. (For a whole-file selection we keep the rename // so that it moves or is discarded together with the contents.) StripRename bool // The indices of lines that should be included in the patch. IncludedLineIndices []int } func transform(patch *Patch, opts TransformOpts) *Patch { transformer := &patchTransformer{ patch: patch, opts: opts, } return transformer.transform() } // helper function that takes a start and end index and returns a slice of all // indexes inbetween (inclusive) func ExpandRange(start int, end int) []int { expanded := []int{} for i := start; i <= end; i++ { expanded = append(expanded, i) } return expanded } func (self *patchTransformer) transform() *Patch { header := self.transformHeader() hunks := self.transformHunks() return &Patch{ header: header, hunks: hunks, } } func (self *patchTransformer) transformHeader() []string { if self.opts.FileNameOverride != "" { return []string{ "--- a/" + self.opts.FileNameOverride, "+++ b/" + self.opts.FileNameOverride, } } header := self.patch.header if self.opts.StripRename { header = stripRenameFromHeader(header) } if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile { result := make([]string, 0, len(header)) for idx, line := range header { if strings.HasPrefix(line, "new file mode") { continue } if line == "--- /dev/null" && strings.HasPrefix(header[idx+1], "+++ b/") { line = "--- a/" + header[idx+1][6:] } result = append(result, line) } return result } return header } // stripRenameFromHeader rewrites a rename diff header so that it looks like a // plain modification of the new path: it drops the rename metadata and points // the diff at the new path on both sides, while keeping the blob index line so // that `git apply --3way` can still fall back to a blob merge. See the // StripRename option for why we do this. func stripRenameFromHeader(header []string) []string { newPath := "" for _, line := range header { if path, ok := strings.CutPrefix(line, "+++ b/"); ok { newPath = path break } } result := make([]string, 0, len(header)) for _, line := range header { switch { case strings.HasPrefix(line, "similarity index "), strings.HasPrefix(line, "dissimilarity index "), strings.HasPrefix(line, "rename from "), strings.HasPrefix(line, "rename to "): // drop the rename metadata case strings.HasPrefix(line, "diff --git "): result = append(result, "diff --git a/"+newPath+" b/"+newPath) case strings.HasPrefix(line, "--- "): result = append(result, "--- a/"+newPath) default: result = append(result, line) } } return result } func (self *patchTransformer) transformHunks() []*Hunk { newHunks := make([]*Hunk, 0, len(self.patch.hunks)) startOffset := 0 var formattedHunk *Hunk for i, hunk := range self.patch.hunks { startOffset, formattedHunk = self.transformHunk( hunk, startOffset, self.patch.HunkStartIdx(i), ) if formattedHunk.containsChanges() { newHunks = append(newHunks, formattedHunk) } } return newHunks } func (self *patchTransformer) transformHunk(hunk *Hunk, startOffset int, firstLineIdx int) (int, *Hunk) { newLines := self.transformHunkLines(hunk, firstLineIdx) newNewStart, newStartOffset := self.transformHunkHeader(newLines, hunk.oldStart, startOffset) newHunk := &Hunk{ bodyLines: newLines, oldStart: hunk.oldStart, newStart: newNewStart, headerContext: hunk.headerContext, } return newStartOffset, newHunk } func (self *patchTransformer) transformHunkLines(hunk *Hunk, firstLineIdx int) []*PatchLine { skippedNewlineMessageIndex := -1 newLines := []*PatchLine{} // Unselected "old-file" lines (deletions when staging, additions when // reverse-staging) are converted to context but buffered here rather than // appended immediately. This ensures they end up after any selected additions // in the same change block, giving the correct output ordering: // [selected deletions] [selected additions] [context from unselected deletions] // Exception: if unselected new-file lines have been skipped earlier in the // current change block, the selected addition comes "later" in the block. In // that case the pending context (from unselected deletions before it) must be // flushed first so those context lines appear before the addition in the output. pendingContext := []*PatchLine{} didSeeUnselectedNewFileLine := false flushPendingContext := func() { newLines = append(newLines, pendingContext...) pendingContext = pendingContext[:0] } for i, line := range hunk.bodyLines { lineIdx := i + firstLineIdx + 1 // plus one for header line if line.Content == "" { break } isLineSelected := lo.Contains(self.opts.IncludedLineIndices, lineIdx) if line.Kind == CONTEXT { flushPendingContext() didSeeUnselectedNewFileLine = false newLines = append(newLines, line) continue } if line.Kind == NEWLINE_MESSAGE { if skippedNewlineMessageIndex != lineIdx { flushPendingContext() newLines = append(newLines, line) } continue } isOldFileLine := (line.Kind == DELETION && !self.opts.Reverse) || (line.Kind == ADDITION && self.opts.Reverse) if isLineSelected { // Selected "old-file" lines must flush pending context first to preserve // the correct ordering of old-file lines (deletions and context) relative // to each other. if isOldFileLine || // Some new-file lines were skipped earlier in this change block, meaning // this selected addition comes after them positionally. Flush pending // context first so the unselected deletion context lines appear before // this addition rather than after it. didSeeUnselectedNewFileLine { flushPendingContext() } newLines = append(newLines, line) continue } if isOldFileLine { content := " " + line.Content[1:] pendingContext = append(pendingContext, &PatchLine{ Kind: CONTEXT, Content: content, }) continue } didSeeUnselectedNewFileLine = true if line.Kind == ADDITION { // 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 } } flushPendingContext() return newLines } func (self *patchTransformer) transformHunkHeader(newBodyLines []*PatchLine, oldStart int, startOffset int) (int, int) { oldLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, DELETION}) newLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, ADDITION}) 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 return newStart, newStartOffset }