From 826d1660c97b7c5c55420ffed21eaa5f16118118 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 15 Aug 2020 11:18:40 +1000 Subject: [PATCH] move patch stuff into its own package --- pkg/commands/commit_file.go | 10 -- pkg/commands/git.go | 9 +- pkg/commands/patch/hunk.go | 142 +++++++++++++++++ pkg/commands/{ => patch}/patch_manager.go | 12 +- pkg/commands/{ => patch}/patch_modifier.go | 146 +----------------- .../{ => patch}/patch_modifier_test.go | 2 +- pkg/commands/{ => patch}/patch_parser.go | 2 +- pkg/commands/patch_rebases.go | 10 +- pkg/gui/gui.go | 3 +- pkg/gui/line_by_line_panel.go | 8 +- pkg/gui/presentation/commit_files.go | 7 +- pkg/gui/staging_panel.go | 4 +- pkg/utils/utils.go | 9 ++ 13 files changed, 188 insertions(+), 176 deletions(-) create mode 100644 pkg/commands/patch/hunk.go rename pkg/commands/{ => patch}/patch_manager.go (94%) rename pkg/commands/{ => patch}/patch_modifier.go (52%) rename pkg/commands/{ => patch}/patch_modifier_test.go (99%) rename pkg/commands/{ => patch}/patch_parser.go (99%) diff --git a/pkg/commands/commit_file.go b/pkg/commands/commit_file.go index 03614be24..aa21d77ee 100644 --- a/pkg/commands/commit_file.go +++ b/pkg/commands/commit_file.go @@ -7,13 +7,3 @@ type CommitFile struct { DisplayString string Status int // one of 'WHOLE' 'PART' 'NONE' } - -const ( - // UNSELECTED is for when the commit file has not been added to the patch in any way - UNSELECTED = iota - // WHOLE is for when you want to add the whole diff of a file to the patch, - // including e.g. if it was deleted - WHOLE = iota - // PART is for when you're only talking about specific lines that have been modified - PART -) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index f4fde0e89..a98ad42d1 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -16,6 +16,7 @@ import ( "github.com/go-errors/errors" gogit "github.com/go-git/go-git/v5" + "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" @@ -84,7 +85,7 @@ type GitCommand struct { removeFile func(string) error DotGitDir string onSuccessfulContinue func() error - PatchManager *PatchManager + PatchManager *patch.PatchManager // Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not PushToCurrent bool @@ -143,7 +144,7 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer, PushToCurrent: pushToCurrent, } - gitCommand.PatchManager = NewPatchManager(log, gitCommand.ApplyPatch) + gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch) return gitCommand, nil } @@ -1038,7 +1039,7 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error { } // GetCommitFiles get the specified commit files -func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) { +func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *patch.PatchManager) ([]*CommitFile, error) { files, err := c.OSCommand.RunCommandWithOutput("git diff-tree --no-commit-id --name-only -r --no-renames %s", commitSha) if err != nil { return nil, err @@ -1047,7 +1048,7 @@ func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager commitFiles := make([]*CommitFile, 0) for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") { - status := UNSELECTED + status := patch.UNSELECTED if patchManager != nil && patchManager.CommitSha == commitSha { status = patchManager.GetFileStatus(file) } diff --git a/pkg/commands/patch/hunk.go b/pkg/commands/patch/hunk.go new file mode 100644 index 000000000..bbb2d54ff --- /dev/null +++ b/pkg/commands/patch/hunk.go @@ -0,0 +1,142 @@ +package patch + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type PatchHunk struct { + FirstLineIdx int + oldStart int + newStart int + heading string + bodyLines []string +} + +func (hunk *PatchHunk) LastLineIdx() int { + return hunk.FirstLineIdx + len(hunk.bodyLines) +} + +func newHunk(lines []string, firstLineIdx int) *PatchHunk { + header := lines[0] + bodyLines := lines[1:] + + oldStart, newStart, heading := headerInfo(header) + + return &PatchHunk{ + oldStart: oldStart, + newStart: newStart, + heading: heading, + FirstLineIdx: firstLineIdx, + bodyLines: bodyLines, + } +} + +func headerInfo(header string) (int, int, string) { + match := hunkHeaderRegexp.FindStringSubmatch(header) + + oldStart := utils.MustConvertToInt(match[1]) + newStart := utils.MustConvertToInt(match[2]) + heading := match[3] + + return oldStart, newStart, heading +} + +func (hunk *PatchHunk) updatedLines(lineIndices []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 + } + isLineSelected := utils.IncludesInt(lineIndices, lineIdx) + + firstChar, content := line[:1], line[1:] + transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected) + + if isLineSelected || (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, isLineSelected bool) string { + if reverse { + if !isLineSelected && firstChar == "+" { + return " " + } else if firstChar == "-" { + return "+" + } else if firstChar == "+" { + return "-" + } else { + return firstChar + } + } + + if !isLineSelected && 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(lineIndices []int, reverse bool, startOffset int) (int, string) { + bodyLines := hunk.updatedLines(lineIndices, 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 := nLinesWithPrefix(newBodyLines, []string{"+", "-"}) + oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"}) + newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "}) + + if changeCount == 0 { + // if nothing has changed we just return nothing + return startOffset, "", false + } + + var oldStart int + if reverse { + oldStart = hunk.newStart + } else { + oldStart = hunk.oldStart + } + + 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, hunk.heading) + return newStartOffset, formattedHeader, true +} diff --git a/pkg/commands/patch_manager.go b/pkg/commands/patch/patch_manager.go similarity index 94% rename from pkg/commands/patch_manager.go rename to pkg/commands/patch/patch_manager.go index 6c1b9ced9..d248381f6 100644 --- a/pkg/commands/patch_manager.go +++ b/pkg/commands/patch/patch_manager.go @@ -1,4 +1,4 @@ -package commands +package patch import ( "sort" @@ -7,6 +7,16 @@ import ( "github.com/sirupsen/logrus" ) +const ( + // UNSELECTED is for when the commit file has not been added to the patch in any way + UNSELECTED = iota + // WHOLE is for when you want to add the whole diff of a file to the patch, + // including e.g. if it was deleted + WHOLE = iota + // PART is for when you're only talking about specific lines that have been modified + PART +) + type fileInfo struct { mode int // one of WHOLE/PART includedLineIndices []int diff --git a/pkg/commands/patch_modifier.go b/pkg/commands/patch/patch_modifier.go similarity index 52% rename from pkg/commands/patch_modifier.go rename to pkg/commands/patch/patch_modifier.go index f4a782015..c2bbc60f6 100644 --- a/pkg/commands/patch_modifier.go +++ b/pkg/commands/patch/patch_modifier.go @@ -1,160 +1,16 @@ -package commands +package patch import ( "fmt" "regexp" - "strconv" "strings" - "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" ) var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`) var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`) -type PatchHunk struct { - FirstLineIdx int - oldStart int - newStart int - heading string - bodyLines []string -} - -func (hunk *PatchHunk) LastLineIdx() int { - return hunk.FirstLineIdx + len(hunk.bodyLines) -} - -func newHunk(lines []string, firstLineIdx int) *PatchHunk { - header := lines[0] - bodyLines := lines[1:] - - oldStart, newStart, heading := headerInfo(header) - - return &PatchHunk{ - oldStart: oldStart, - newStart: newStart, - heading: heading, - FirstLineIdx: firstLineIdx, - bodyLines: bodyLines, - } -} - -func headerInfo(header string) (int, int, string) { - match := hunkHeaderRegexp.FindStringSubmatch(header) - - oldStart := mustConvertToInt(match[1]) - newStart := mustConvertToInt(match[2]) - heading := match[3] - - return oldStart, newStart, heading -} - -func (hunk *PatchHunk) updatedLines(lineIndices []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 - } - isLineSelected := utils.IncludesInt(lineIndices, lineIdx) - - firstChar, content := line[:1], line[1:] - transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected) - - if isLineSelected || (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, isLineSelected bool) string { - if reverse { - if !isLineSelected && firstChar == "+" { - return " " - } else if firstChar == "-" { - return "+" - } else if firstChar == "+" { - return "-" - } else { - return firstChar - } - } - - if !isLineSelected && 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(lineIndices []int, reverse bool, startOffset int) (int, string) { - bodyLines := hunk.updatedLines(lineIndices, 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 := nLinesWithPrefix(newBodyLines, []string{"+", "-"}) - oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"}) - newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "}) - - if changeCount == 0 { - // if nothing has changed we just return nothing - return startOffset, "", false - } - - var oldStart int - if reverse { - oldStart = hunk.newStart - } else { - oldStart = hunk.oldStart - } - - 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, hunk.heading) - return newStartOffset, formattedHeader, true -} - -func mustConvertToInt(s string) int { - i, err := strconv.Atoi(s) - if err != nil { - panic(err) - } - return i -} - func GetHeaderFromDiff(diff string) string { match := patchHeaderRegexp.FindStringSubmatch(diff) if len(match) <= 1 { diff --git a/pkg/commands/patch_modifier_test.go b/pkg/commands/patch/patch_modifier_test.go similarity index 99% rename from pkg/commands/patch_modifier_test.go rename to pkg/commands/patch/patch_modifier_test.go index 66b675923..8b866019b 100644 --- a/pkg/commands/patch_modifier_test.go +++ b/pkg/commands/patch/patch_modifier_test.go @@ -1,4 +1,4 @@ -package commands +package patch import ( "fmt" diff --git a/pkg/commands/patch_parser.go b/pkg/commands/patch/patch_parser.go similarity index 99% rename from pkg/commands/patch_parser.go rename to pkg/commands/patch/patch_parser.go index b54c57c1f..55554fec3 100644 --- a/pkg/commands/patch_parser.go +++ b/pkg/commands/patch/patch_parser.go @@ -1,4 +1,4 @@ -package commands +package patch import ( "regexp" diff --git a/pkg/commands/patch_rebases.go b/pkg/commands/patch_rebases.go index bb1ca5b98..f4d411514 100644 --- a/pkg/commands/patch_rebases.go +++ b/pkg/commands/patch_rebases.go @@ -2,11 +2,13 @@ package commands import ( "fmt" + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/commands/patch" ) // DeletePatchesFromCommit applies a patch in reverse for a commit -func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error { +func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *patch.PatchManager) error { if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { return err } @@ -33,7 +35,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, return c.GenericMerge("rebase", "continue") } -func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error { +func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *patch.PatchManager) error { if sourceCommitIdx < destinationCommitIdx { if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil { return err @@ -134,7 +136,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitId return c.GenericMerge("rebase", "continue") } -func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager, stash bool) error { +func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *patch.PatchManager, stash bool) error { if stash { if err := c.StashSave(c.Tr.SLocalize("StashPrefix") + commits[commitIdx].Sha); err != nil { return err @@ -187,7 +189,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *Pat return c.GenericMerge("rebase", "continue") } -func (c *GitCommand) PullPatchIntoNewCommit(commits []*Commit, commitIdx int, p *PatchManager) error { +func (c *GitCommand) PullPatchIntoNewCommit(commits []*Commit, commitIdx int, p *patch.PatchManager) error { if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { return err } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 52e24e298..71cda50f7 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -22,6 +22,7 @@ import ( "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/tasks" @@ -102,7 +103,7 @@ type lineByLinePanelState struct { FirstLineIdx int LastLineIdx int Diff string - PatchParser *commands.PatchParser + PatchParser *patch.PatchParser SelectMode int // one of LINE, HUNK, or RANGE SecondaryFocused bool // this is for if we show the left or right panel } diff --git a/pkg/gui/line_by_line_panel.go b/pkg/gui/line_by_line_panel.go index d914d0085..0c4c5651c 100644 --- a/pkg/gui/line_by_line_panel.go +++ b/pkg/gui/line_by_line_panel.go @@ -5,7 +5,7 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/patch" ) // Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'. @@ -27,7 +27,7 @@ const ( func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int) (bool, error) { state := gui.State.Panels.LineByLine - patchParser, err := commands.NewPatchParser(gui.Log, diff) + patchParser, err := patch.NewPatchParser(gui.Log, diff) if err != nil { return false, nil } @@ -85,7 +85,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second secondaryView.Highlight = true secondaryView.Wrap = false - secondaryPatchParser, err := commands.NewPatchParser(gui.Log, secondaryDiff) + secondaryPatchParser, err := patch.NewPatchParser(gui.Log, secondaryDiff) if err != nil { return false, nil } @@ -120,7 +120,7 @@ func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error { return gui.selectNewHunk(newHunk) } -func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error { +func (gui *Gui) selectNewHunk(newHunk *patch.PatchHunk) error { state := gui.State.Panels.LineByLine state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx) if state.SelectMode == HUNK { diff --git a/pkg/gui/presentation/commit_files.go b/pkg/gui/presentation/commit_files.go index 3f16da965..59e026f16 100644 --- a/pkg/gui/presentation/commit_files.go +++ b/pkg/gui/presentation/commit_files.go @@ -3,6 +3,7 @@ package presentation import ( "github.com/fatih/color" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/theme" ) @@ -26,11 +27,11 @@ func getCommitFileDisplayStrings(f *commands.CommitFile, diffed bool) []string { var colour *color.Color switch f.Status { - case commands.UNSELECTED: + case patch.UNSELECTED: colour = defaultColor - case commands.WHOLE: + case patch.WHOLE: colour = green - case commands.PART: + case patch.PART: colour = yellow } if diffed { diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index f8b556743..8bb586a9d 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -4,7 +4,7 @@ import ( "strings" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/patch" ) func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int) error { @@ -130,7 +130,7 @@ func (gui *Gui) applySelection(reverse bool) error { return err } - patch := commands.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false) + patch := patch.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false) if patch == "" { return nil diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 6eca59a6b..12c89e5da 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" "time" @@ -334,3 +335,11 @@ func StringArraysOverlap(strArrA []string, strArrB []string) bool { return false } + +func MustConvertToInt(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return i +}