package commands import ( "sort" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" ) type fileInfo struct { mode int // one of WHOLE/PART includedLineIndices []int diff string } type applyPatchFunc func(patch string, flags ...string) error // PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit) type PatchManager struct { CommitSha string fileInfoMap map[string]*fileInfo Log *logrus.Entry ApplyPatch applyPatchFunc } // NewPatchManager returns a new PatchModifier func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager { return &PatchManager{ Log: log, ApplyPatch: applyPatch, } } // NewPatchManager returns a new PatchModifier func (p *PatchManager) Start(commitSha string, diffMap map[string]string) { p.CommitSha = commitSha p.fileInfoMap = map[string]*fileInfo{} for filename, diff := range diffMap { p.fileInfoMap[filename] = &fileInfo{ mode: UNSELECTED, diff: diff, } } } func (p *PatchManager) AddFile(filename string) { p.fileInfoMap[filename].mode = WHOLE p.fileInfoMap[filename].includedLineIndices = nil } func (p *PatchManager) RemoveFile(filename string) { p.fileInfoMap[filename].mode = UNSELECTED p.fileInfoMap[filename].includedLineIndices = nil } func (p *PatchManager) ToggleFileWhole(filename string) { info := p.fileInfoMap[filename] switch info.mode { case UNSELECTED: p.AddFile(filename) case WHOLE: p.RemoveFile(filename) case PART: p.AddFile(filename) } } func getIndicesForRange(first, last int) []int { indices := []int{} for i := first; i <= last; i++ { indices = append(indices, i) } return indices } func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) { info := p.fileInfoMap[filename] info.mode = PART info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx)) } func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) { info := p.fileInfoMap[filename] info.mode = PART info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx)) if len(info.includedLineIndices) == 0 { p.RemoveFile(filename) } } func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string { info := p.fileInfoMap[filename] if info == nil { return "" } switch info.mode { case WHOLE: // use the whole diff // the reverse flag is only for part patches so we're ignoring it here return info.diff case PART: // generate a new diff with just the selected lines m := NewPatchModifier(p.Log, filename, info.diff) return m.ModifiedPatchForLines(info.includedLineIndices, reverse, keepOriginalHeader) default: return "" } } func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string { patch := p.RenderPlainPatchForFile(filename, reverse, keepOriginalHeader) if plain { return patch } parser, err := NewPatchParser(p.Log, patch) if err != nil { // swallowing for now return "" } // not passing included lines because we don't want to see them in the secondary panel return parser.Render(-1, -1, nil) } func (p *PatchManager) RenderEachFilePatch(plain bool) []string { // sort files by name then iterate through and render each patch filenames := make([]string, len(p.fileInfoMap)) index := 0 for filename := range p.fileInfoMap { filenames[index] = filename index++ } sort.Strings(filenames) output := []string{} for _, filename := range filenames { patch := p.RenderPatchForFile(filename, plain, false, true) if patch != "" { output = append(output, patch) } } return output } func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string { result := "" for _, patch := range p.RenderEachFilePatch(plain) { if patch != "" { result += patch + "\n" } } return result } func (p *PatchManager) GetFileStatus(filename string) int { info := p.fileInfoMap[filename] if info == nil { return UNSELECTED } return info.mode } func (p *PatchManager) GetFileIncLineIndices(filename string) []int { info := p.fileInfoMap[filename] if info == nil { return []int{} } return info.includedLineIndices } func (p *PatchManager) ApplyPatches(reverse bool) error { // for whole patches we'll apply the patch in reverse // but for part patches we'll apply a reverse patch forwards for filename, info := range p.fileInfoMap { if info.mode == UNSELECTED { continue } applyFlags := []string{"index", "3way"} reverseOnGenerate := false if reverse { if info.mode == WHOLE { applyFlags = append(applyFlags, "reverse") } else { reverseOnGenerate = true } } var err error // first run we try with the original header, then without for _, keepOriginalHeader := range []bool{true, false} { patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader) if patch == "" { continue } if err = p.ApplyPatch(patch, applyFlags...); err != nil { continue } break } if err != nil { return err } } return nil } // clears the patch func (p *PatchManager) Reset() { p.CommitSha = "" p.fileInfoMap = map[string]*fileInfo{} } func (p *PatchManager) CommitSelected() bool { return p.CommitSha != "" } func (p *PatchManager) IsEmpty() bool { for _, fileInfo := range p.fileInfoMap { if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) { return false } } return true }