diff --git a/pkg/git/branch_list_builder.go b/pkg/commands/branch_list_builder.go similarity index 75% rename from pkg/git/branch_list_builder.go rename to pkg/commands/branch_list_builder.go index 09c95ba97..d7a232055 100644 --- a/pkg/git/branch_list_builder.go +++ b/pkg/commands/branch_list_builder.go @@ -1,10 +1,9 @@ -package git +package commands import ( "regexp" "strings" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" @@ -26,28 +25,28 @@ import ( // BranchListBuilder returns a list of Branch objects for the current repo type BranchListBuilder struct { Log *logrus.Entry - GitCommand *commands.GitCommand + GitCommand *GitCommand } // NewBranchListBuilder builds a new branch list builder -func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) { +func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) { return &BranchListBuilder{ Log: log, GitCommand: gitCommand, }, nil } -func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch { +func (b *BranchListBuilder) obtainCurrentBranch() *Branch { branchName, err := b.GitCommand.CurrentBranchName() if err != nil { panic(err.Error()) } - return &commands.Branch{Name: strings.TrimSpace(branchName)} + return &Branch{Name: strings.TrimSpace(branchName)} } -func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch { - branches := make([]*commands.Branch, 0) +func (b *BranchListBuilder) obtainReflogBranches() []*Branch { + branches := make([]*Branch, 0) rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") if err != nil { return branches @@ -57,14 +56,14 @@ func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch { for _, line := range branchLines { timeNumber, timeUnit, branchName := branchInfoFromLine(line) timeUnit = abbreviatedTimeUnit(timeUnit) - branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit} + branch := &Branch{Name: branchName, Recency: timeNumber + timeUnit} branches = append(branches, branch) } return uniqueByName(branches) } -func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch { - branches := make([]*commands.Branch, 0) +func (b *BranchListBuilder) obtainSafeBranches() []*Branch { + branches := make([]*Branch, 0) bIter, err := b.GitCommand.Repo.Branches() if err != nil { @@ -72,14 +71,14 @@ func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch { } bIter.ForEach(func(b *plumbing.Reference) error { name := b.Name().Short() - branches = append(branches, &commands.Branch{Name: name}) + branches = append(branches, &Branch{Name: name}) return nil }) return branches } -func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch { +func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*Branch, included bool) []*Branch { for _, newBranch := range newBranches { if included == branchIncluded(newBranch.Name, existingBranches) { finalBranches = append(finalBranches, newBranch) @@ -88,7 +87,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi return finalBranches } -func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string { +func sanitisedReflogName(reflogBranch *Branch, safeBranches []*Branch) string { for _, safeBranch := range safeBranches { if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) { return safeBranch.Name @@ -98,8 +97,8 @@ func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands } // Build the list of branches for the current repo -func (b *BranchListBuilder) Build() []*commands.Branch { - branches := make([]*commands.Branch, 0) +func (b *BranchListBuilder) Build() []*Branch { + branches := make([]*Branch, 0) head := b.obtainCurrentBranch() safeBranches := b.obtainSafeBranches() @@ -112,7 +111,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch { branches = b.appendNewBranches(branches, safeBranches, branches, false) if len(branches) == 0 || branches[0].Name != head.Name { - branches = append([]*commands.Branch{head}, branches...) + branches = append([]*Branch{head}, branches...) } branches[0].Recency = " *" @@ -120,7 +119,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch { return branches } -func branchIncluded(branchName string, branches []*commands.Branch) bool { +func branchIncluded(branchName string, branches []*Branch) bool { for _, existingBranch := range branches { if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { return true @@ -129,8 +128,8 @@ func branchIncluded(branchName string, branches []*commands.Branch) bool { return false } -func uniqueByName(branches []*commands.Branch) []*commands.Branch { - finalBranches := make([]*commands.Branch, 0) +func uniqueByName(branches []*Branch) []*Branch { + finalBranches := make([]*Branch, 0) for _, branch := range branches { if branchIncluded(branch.Name, finalBranches) { continue diff --git a/pkg/commands/commit_file.go b/pkg/commands/commit_file.go index 8bc6a11c2..ddd09b23b 100644 --- a/pkg/commands/commit_file.go +++ b/pkg/commands/commit_file.go @@ -1,13 +1,42 @@ package commands +import ( + "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/theme" +) + // CommitFile : A git commit file type CommitFile struct { Sha string Name string 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 +) + // GetDisplayStrings is a function. func (f *CommitFile) GetDisplayStrings(isFocused bool) []string { - return []string{f.DisplayString} + yellow := color.New(color.FgYellow) + green := color.New(color.FgGreen) + defaultColor := color.New(theme.DefaultTextColor) + + var colour *color.Color + switch f.Status { + case UNSELECTED: + colour = defaultColor + case WHOLE: + colour = green + case PART: + colour = yellow + } + return []string{colour.Sprint(f.DisplayString)} } diff --git a/pkg/git/commit_list_builder.go b/pkg/commands/commit_list_builder.go similarity index 86% rename from pkg/git/commit_list_builder.go rename to pkg/commands/commit_list_builder.go index a47fcbfe2..aab6de6a3 100644 --- a/pkg/git/commit_list_builder.go +++ b/pkg/commands/commit_list_builder.go @@ -1,4 +1,4 @@ -package git +package commands import ( "fmt" @@ -9,7 +9,6 @@ import ( "strings" "github.com/fatih/color" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" @@ -27,15 +26,15 @@ import ( // CommitListBuilder returns a list of Branch objects for the current repo type CommitListBuilder struct { Log *logrus.Entry - GitCommand *commands.GitCommand - OSCommand *commands.OSCommand + GitCommand *GitCommand + OSCommand *OSCommand Tr *i18n.Localizer - CherryPickedCommits []*commands.Commit - DiffEntries []*commands.Commit + CherryPickedCommits []*Commit + DiffEntries []*Commit } // NewCommitListBuilder builds a new commit list builder -func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedCommits []*commands.Commit, diffEntries []*commands.Commit) (*CommitListBuilder, error) { +func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) { return &CommitListBuilder{ Log: log, GitCommand: gitCommand, @@ -47,9 +46,9 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, os } // GetCommits obtains the commits of the current branch -func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { - commits := []*commands.Commit{} - var rebasingCommits []*commands.Commit +func (c *CommitListBuilder) GetCommits() ([]*Commit, error) { + commits := []*Commit{} + var rebasingCommits []*Commit rebaseMode, err := c.GitCommand.RebaseMode() if err != nil { return nil, err @@ -74,7 +73,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { sha := splitLine[0] _, unpushed := unpushedCommits[sha] status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed] - commits = append(commits, &commands.Commit{ + commits = append(commits, &Commit{ Sha: sha, Name: strings.Join(splitLine[1:], " "), Status: status, @@ -110,7 +109,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { } // getRebasingCommits obtains the commits that we're in the process of rebasing -func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) { +func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) { switch rebaseMode { case "normal": return c.getNormalRebasingCommits() @@ -121,7 +120,7 @@ func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.C } } -func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) { +func (c *CommitListBuilder) getNormalRebasingCommits() ([]*Commit, error) { rewrittenCount := 0 bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir)) if err == nil { @@ -130,7 +129,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro } // we know we're rebasing, so lets get all the files whose names have numbers - commits := []*commands.Commit{} + commits := []*Commit{} err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), func(path string, f os.FileInfo, err error) error { if rewrittenCount > 0 { rewrittenCount-- @@ -152,7 +151,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro if err != nil { return err } - commits = append([]*commands.Commit{commit}, commits...) + commits = append([]*Commit{commit}, commits...) return nil }) if err != nil { @@ -174,7 +173,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro // getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files // and extracts out the sha and names of commits that we still have to go // in the rebase: -func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) { +func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) { bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir)) if err != nil { c.Log.Info(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error())) @@ -182,14 +181,14 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, return nil, nil } - commits := []*commands.Commit{} + commits := []*Commit{} lines := strings.Split(string(bytesContent), "\n") for _, line := range lines { if line == "" || line == "noop" { return commits, nil } splitLine := strings.Split(line, " ") - commits = append([]*commands.Commit{{ + commits = append([]*Commit{{ Sha: splitLine[1][0:7], Name: strings.Join(splitLine[2:], " "), Status: "rebasing", @@ -205,18 +204,18 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, // From: Lazygit Tester // Date: Wed, 5 Dec 2018 21:03:23 +1100 // Subject: second commit on master -func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) { +func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) { lines := strings.Split(content, "\n") sha := strings.Split(lines[0], " ")[1][0:7] name := strings.TrimPrefix(lines[3], "Subject: ") - return &commands.Commit{ + return &Commit{ Sha: sha, Name: name, Status: "rebasing", }, nil } -func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.Commit, error) { +func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) { ancestor, err := c.getMergeBase() if err != nil { return nil, err @@ -239,7 +238,7 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) return commits, nil } -func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) { +func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) { for _, commit := range commits { for _, cherryPickedCommit := range c.CherryPickedCommits { if commit.Sha == cherryPickedCommit.Sha { diff --git a/pkg/git/commit_list_builder_test.go b/pkg/commands/commit_list_builder_test.go similarity index 93% rename from pkg/git/commit_list_builder_test.go rename to pkg/commands/commit_list_builder_test.go index 81bb7b3cb..cdd360ce8 100644 --- a/pkg/git/commit_list_builder_test.go +++ b/pkg/commands/commit_list_builder_test.go @@ -1,24 +1,23 @@ -package git +package commands import ( "os/exec" "testing" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/stretchr/testify/assert" ) // NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing func NewDummyCommitListBuilder() *CommitListBuilder { - osCommand := commands.NewDummyOSCommand() + osCommand := NewDummyOSCommand() return &CommitListBuilder{ - Log: commands.NewDummyLog(), - GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand), + Log: NewDummyLog(), + GitCommand: NewDummyGitCommandWithOSCommand(osCommand), OSCommand: osCommand, - Tr: i18n.NewLocalizer(commands.NewDummyLog()), - CherryPickedCommits: []*commands.Commit{}, + Tr: i18n.NewLocalizer(NewDummyLog()), + CherryPickedCommits: []*Commit{}, } } @@ -199,7 +198,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) { type scenario struct { testName string command func(string, ...string) *exec.Cmd - test func([]*commands.Commit, error) + test func([]*Commit, error) } scenarios := []scenario{ @@ -225,7 +224,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) { return nil }, - func(commits []*commands.Commit, err error) { + func(commits []*Commit, err error) { assert.NoError(t, err) assert.Len(t, commits, 0) }, @@ -252,10 +251,10 @@ func TestCommitListBuilderGetCommits(t *testing.T) { return nil }, - func(commits []*commands.Commit, err error) { + func(commits []*Commit, err error) { assert.NoError(t, err) assert.Len(t, commits, 2) - assert.EqualValues(t, []*commands.Commit{ + assert.EqualValues(t, []*Commit{ { Sha: "8a2bb0e", Name: "commit 1", @@ -298,7 +297,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) { return nil }, - func(commits []*commands.Commit, err error) { + func(commits []*Commit, err error) { assert.Error(t, err) assert.Len(t, commits, 0) }, diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 6e86fe0b5..815c84423 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -63,16 +63,17 @@ func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repositor // GitCommand is our main git interface type GitCommand struct { - Log *logrus.Entry - OSCommand *OSCommand - Worktree *gogit.Worktree - Repo *gogit.Repository - Tr *i18n.Localizer - Config config.AppConfigurer - getGlobalGitConfig func(string) (string, error) - getLocalGitConfig func(string) (string, error) - removeFile func(string) error - DotGitDir string + Log *logrus.Entry + OSCommand *OSCommand + Worktree *gogit.Worktree + Repo *gogit.Repository + Tr *i18n.Localizer + Config config.AppConfigurer + getGlobalGitConfig func(string) (string, error) + getLocalGitConfig func(string) (string, error) + removeFile func(string) error + DotGitDir string + onSuccessfulContinue func() error } // NewGitCommand it runs git commands @@ -376,7 +377,7 @@ func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) { // AmendHead amends HEAD with whatever is staged in your working tree func (c *GitCommand) AmendHead() (*exec.Cmd, error) { - command := "git commit --amend --no-edit" + command := "git commit --amend --no-edit --allow-empty" if c.usingGpg() { return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil } @@ -530,7 +531,7 @@ func (c *GitCommand) Ignore(filename string) error { // Show shows the diff of a commit func (c *GitCommand) Show(sha string) (string, error) { - show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha)) + show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color --no-renames %s", sha)) if err != nil { return "", err } @@ -605,11 +606,11 @@ func (c *GitCommand) Diff(file *File, plain bool, cached bool) string { return s } -func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string, error) { +func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool, extraFlags string) error { filename, err := c.OSCommand.CreateTempFile("patch", patch) if err != nil { c.Log.Error(err) - return "", err + return err } defer func() { _ = c.OSCommand.Remove(filename) }() @@ -624,7 +625,7 @@ func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string cachedFlag = "--cached" } - return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply %s %s %s", cachedFlag, reverseFlag, c.OSCommand.Quote(filename))) + return c.OSCommand.RunCommand(fmt.Sprintf("git apply %s %s %s %s", cachedFlag, reverseFlag, extraFlags, c.OSCommand.Quote(filename))) } func (c *GitCommand) FastForward(branchName string) error { @@ -645,13 +646,29 @@ func (c *GitCommand) RunSkipEditorCommand(command string) error { // GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" // By default we skip the editor in the case where a commit will be made func (c *GitCommand) GenericMerge(commandType string, command string) error { - return c.RunSkipEditorCommand( + err := c.RunSkipEditorCommand( fmt.Sprintf( "git %s --%s", commandType, command, ), ) + if err != nil { + return err + } + + // sometimes we need to do a sequence of things in a rebase but the user needs to + // fix merge conflicts along the way. When this happens we queue up the next step + // so that after the next successful rebase continue we can continue from where we left off + if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil { + f := c.onSuccessfulContinue + c.onSuccessfulContinue = nil + return f() + } + if command == "abort" { + c.onSuccessfulContinue = nil + } + return nil } func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) { @@ -852,8 +869,8 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error { } // GetCommitFiles get the specified commit files -func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) { - cmd := fmt.Sprintf("git show --pretty= --name-only %s", commitSha) +func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) { + cmd := fmt.Sprintf("git show --pretty= --name-only --no-renames %s", commitSha) files, err := c.OSCommand.RunCommandWithOutput(cmd) if err != nil { return nil, err @@ -862,10 +879,16 @@ func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) { commitFiles := make([]*CommitFile, 0) for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") { + status := UNSELECTED + if patchManager != nil && patchManager.CommitSha == commitSha { + status = patchManager.GetFileStatus(file) + } + commitFiles = append(commitFiles, &CommitFile{ Sha: commitSha, Name: file, DisplayString: file, + Status: status, }) } @@ -873,8 +896,12 @@ func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) { } // ShowCommitFile get the diff of specified commit file -func (c *GitCommand) ShowCommitFile(commitSha, fileName string) (string, error) { - cmd := fmt.Sprintf("git show --color %s -- %s", commitSha, fileName) +func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) { + colorArg := "--color" + if plain { + colorArg = "" + } + cmd := fmt.Sprintf("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName) return c.OSCommand.RunCommandWithOutput(cmd) } @@ -886,28 +913,7 @@ func (c *GitCommand) CheckoutFile(commitSha, fileName string) error { // DiscardOldFileChanges discards changes to a file from an old commit func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error { - if len(commits)-1 < commitIndex { - return errors.New("index outside of range of commits") - } - - // we can make this GPG thing possible it just means we need to do this in two parts: - // one where we handle the possibility of a credential request, and the other - // where we continue the rebase - if c.usingGpg() { - return errors.New(c.Tr.SLocalize("DisabledForGPG")) - } - - todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit") - if err != nil { - return err - } - - cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) - if err != nil { - return err - } - - if err := c.OSCommand.RunPreparedCommand(cmd); err != nil { + if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { return err } @@ -924,7 +930,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, f } // amend the commit - cmd, err = c.AmendHead() + cmd, err := c.AmendHead() if cmd != nil { return errors.New("received unexpected pointer to cmd") } @@ -1016,3 +1022,34 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error { return nil } + +// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current +// commit and pick all others. After this you'll want to call `c.GenericMerge("rebase", "continue")` +func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*Commit, commitIndex int) error { + if len(commits)-1 < commitIndex { + return errors.New("index outside of range of commits") + } + + // we can make this GPG thing possible it just means we need to do this in two parts: + // one where we handle the possibility of a credential request, and the other + // where we continue the rebase + if c.usingGpg() { + return errors.New(c.Tr.SLocalize("DisabledForGPG")) + } + + todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit") + if err != nil { + return err + } + + cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) + if err != nil { + return err + } + + if err := c.OSCommand.RunPreparedCommand(cmd); err != nil { + return err + } + + return nil +} diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index c132de98a..52db798ed 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -1685,7 +1685,7 @@ func TestGitCommandApplyPatch(t *testing.T) { type scenario struct { testName string command func(string, ...string) *exec.Cmd - test func(string, error) + test func(error) } scenarios := []scenario{ @@ -1702,9 +1702,8 @@ func TestGitCommandApplyPatch(t *testing.T) { return exec.Command("echo", "done") }, - func(output string, err error) { + func(err error) { assert.NoError(t, err) - assert.EqualValues(t, "done\n", output) }, }, { @@ -1724,7 +1723,7 @@ func TestGitCommandApplyPatch(t *testing.T) { return exec.Command("test") }, - func(output string, err error) { + func(err error) { assert.Error(t, err) }, }, @@ -1734,7 +1733,7 @@ func TestGitCommandApplyPatch(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommand() gitCmd.OSCommand.command = s.command - s.test(gitCmd.ApplyPatch("test", false, true)) + s.test(gitCmd.ApplyPatch("test", false, true, "")) }) } } @@ -1962,7 +1961,7 @@ func TestGitCommandShowCommitFile(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd.OSCommand.command = s.command - s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName)) + s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName, true)) }) } } @@ -2001,7 +2000,7 @@ func TestGitCommandGetCommitFiles(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd.OSCommand.command = s.command - s.test(gitCmd.GetCommitFiles(s.commitSha)) + s.test(gitCmd.GetCommitFiles(s.commitSha, nil)) }) } } diff --git a/pkg/commands/patch_manager.go b/pkg/commands/patch_manager.go new file mode 100644 index 000000000..7c0da245d --- /dev/null +++ b/pkg/commands/patch_manager.go @@ -0,0 +1,194 @@ +package commands + +import ( + "sort" + "strings" + + "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, reverse bool, cached bool, extraFlags 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, commitSha string, diffMap map[string]string) *PatchManager { + infoMap := map[string]*fileInfo{} + for filename, diff := range diffMap { + infoMap[filename] = &fileInfo{ + mode: UNSELECTED, + diff: diff, + } + } + + return &PatchManager{ + Log: log, + fileInfoMap: infoMap, + CommitSha: commitSha, + ApplyPatch: applyPatch, + } +} + +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) 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, true) + default: + return "" + } +} + +func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool) string { + patch := p.RenderPlainPatchForFile(filename, reverse) + 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) + if patch != "" { + output = append(output, patch) + } + } + + return output +} + +func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string { + return strings.Join(p.RenderEachFilePatch(plain), "\n") +} + +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 + } + + reverseOnGenerate := false + reverseOnApply := false + if reverse { + if info.mode == WHOLE { + reverseOnApply = true + } else { + reverseOnGenerate = true + } + } + + patch := p.RenderPatchForFile(filename, true, reverseOnGenerate) + if patch == "" { + continue + } + p.Log.Warn(patch) + if err := p.ApplyPatch(patch, reverseOnApply, false, "--index --3way"); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/git/patch_modifier.go b/pkg/commands/patch_modifier.go similarity index 80% rename from pkg/git/patch_modifier.go rename to pkg/commands/patch_modifier.go index b6c1b7184..e407199c0 100644 --- a/pkg/git/patch_modifier.go +++ b/pkg/commands/patch_modifier.go @@ -1,4 +1,4 @@ -package git +package commands import ( "fmt" @@ -10,7 +10,8 @@ import ( "github.com/sirupsen/logrus" ) -var headerRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`) +var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`) +var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`) type PatchHunk struct { header string @@ -116,7 +117,7 @@ func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, rev } // get oldstart, newstart, and heading from header - match := headerRegexp.FindStringSubmatch(hunk.header) + match := hunkHeaderRegexp.FindStringSubmatch(hunk.header) var oldStart int if reverse { @@ -152,9 +153,17 @@ func mustConvertToInt(s string) int { return i } +func GetHeaderFromDiff(diff string) string { + match := patchHeaderRegexp.FindStringSubmatch(diff) + if len(match) <= 1 { + return "" + } + return match[1] +} + func GetHunksFromDiff(diff string) []*PatchHunk { - headers := headerRegexp.FindAllString(diff, -1) - bodies := headerRegexp.Split(diff, -1)[1:] // discarding top bit + headers := hunkHeaderRegexp.FindAllString(diff, -1) + bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit headerFirstLineIndices := []int{} for lineIdx, line := range strings.Split(diff, "\n") { @@ -175,6 +184,7 @@ type PatchModifier struct { Log *logrus.Entry filename string hunks []*PatchHunk + header string } func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier { @@ -182,10 +192,11 @@ func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *Patc Log: log, filename: filename, hunks: GetHunksFromDiff(diffText), + header: GetHeaderFromDiff(diffText), } } -func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool) string { +func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string { // step one is getting only those hunks which we care about hunksInRange := []*PatchHunk{} outer: @@ -212,21 +223,38 @@ outer: return "" } - fileHeader := fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename) + var fileHeader string + // 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 + if keepOriginalHeader { + fileHeader = d.header + } else { + fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename) + } return fileHeader + formattedHunks } -func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool) string { +func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string { // generate array of consecutive line indices from our range selectedLines := []int{} for i := firstLineIdx; i <= lastLineIdx; i++ { selectedLines = append(selectedLines, i) } - return d.ModifiedPatchForLines(selectedLines, reverse) + return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader) } -func ModifiedPatchForRange(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) +func (d *PatchModifier) OriginalPatchLength() int { + if len(d.hunks) == 0 { + return 0 + } + + return d.hunks[len(d.hunks)-1].LastLineIdx +} + +func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string { + p := NewPatchModifier(log, filename, diffText) + return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader) } diff --git a/pkg/git/patch_modifier_test.go b/pkg/commands/patch_modifier_test.go similarity index 99% rename from pkg/git/patch_modifier_test.go rename to pkg/commands/patch_modifier_test.go index a9381c721..87f1000a9 100644 --- a/pkg/git/patch_modifier_test.go +++ b/pkg/commands/patch_modifier_test.go @@ -1,4 +1,4 @@ -package git +package commands import ( "fmt" @@ -502,7 +502,7 @@ func TestModifyPatchForRange(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { - result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse) + result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, true) if !assert.Equal(t, s.expected, result) { fmt.Println(result) } diff --git a/pkg/git/patch_parser.go b/pkg/commands/patch_parser.go similarity index 74% rename from pkg/git/patch_parser.go rename to pkg/commands/patch_parser.go index 21091d1c2..06781c8cf 100644 --- a/pkg/git/patch_parser.go +++ b/pkg/commands/patch_parser.go @@ -1,4 +1,4 @@ -package git +package commands import ( "regexp" @@ -12,6 +12,8 @@ import ( const ( PATCH_HEADER = iota + COMMIT_SHA + COMMIT_DESCRIPTION HUNK_HEADER ADDITION DELETION @@ -81,7 +83,10 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun return p.PatchHunks[0] } -func (l *PatchLine) render(selected bool) string { +// selected means you've got it highlighted with your cursor +// included means the line has been included in the patch (only applicable when +// building a patch) +func (l *PatchLine) render(selected bool, included bool) string { content := l.Content if len(content) == 0 { content = " " // using the space so that we can still highlight if necessary @@ -91,7 +96,7 @@ func (l *PatchLine) render(selected bool) string { if l.Kind == HUNK_HEADER { re := regexp.MustCompile("(@@.*?@@)(.*)") match := re.FindStringSubmatch(content) - return coloredString(color.FgCyan, match[1], selected) + coloredString(theme.DefaultTextColor, match[2], selected) + return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false) } var colorAttr color.Attribute @@ -102,21 +107,34 @@ func (l *PatchLine) render(selected bool) string { colorAttr = color.FgGreen case DELETION: colorAttr = color.FgRed + case COMMIT_SHA: + colorAttr = color.FgYellow default: colorAttr = theme.DefaultTextColor } - return coloredString(colorAttr, content, selected) + return coloredString(colorAttr, content, selected, included) } -func coloredString(colorAttr color.Attribute, str string, selected bool) string { +func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string { var cl *color.Color + attributes := []color.Attribute{colorAttr} if selected { - cl = color.New(colorAttr, color.BgBlue) - } else { - cl = color.New(colorAttr) + attributes = append(attributes, color.BgBlue) } - return utils.ColoredStringDirect(str, cl) + cl = color.New(attributes...) + var clIncluded *color.Color + if included { + clIncluded = color.New(append(attributes, color.BgGreen)...) + } else { + clIncluded = color.New(attributes...) + } + + if len(str) < 2 { + return utils.ColoredStringDirect(str, clIncluded) + } + + return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl) } func parsePatch(patch string) ([]int, []int, []*PatchLine, error) { @@ -124,16 +142,26 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) { hunkStarts := []int{} stageableLines := []int{} pastFirstHunkHeader := false + pastCommitDescription := true patchLines := make([]*PatchLine, len(lines)) var lineKind int var firstChar string for index, line := range lines { - lineKind = PATCH_HEADER firstChar = " " if len(line) > 0 { firstChar = line[:1] } - if firstChar == "@" { + if index == 0 && strings.HasPrefix(line, "commit") { + lineKind = COMMIT_SHA + pastCommitDescription = false + } else if !pastCommitDescription { + if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") { + pastCommitDescription = true + lineKind = PATCH_HEADER + } else { + lineKind = COMMIT_DESCRIPTION + } + } else if firstChar == "@" { pastFirstHunkHeader = true hunkStarts = append(hunkStarts, index) lineKind = HUNK_HEADER @@ -150,6 +178,8 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) { case " ": lineKind = CONTEXT } + } else { + lineKind = PATCH_HEADER } patchLines[index] = &PatchLine{Kind: lineKind, Content: line} } @@ -157,11 +187,12 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) { } // Render returns the coloured string of the diff with any selected lines highlighted -func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int) string { +func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string { renderedLines := make([]string, len(p.PatchLines)) for index, patchLine := range p.PatchLines { selected := index >= firstLineIndex && index <= lastLineIndex - renderedLines[index] = patchLine.render(selected) + included := utils.IncludesInt(incLineIndices, index) + renderedLines[index] = patchLine.render(selected, included) } return strings.Join(renderedLines, "\n") } diff --git a/pkg/commands/patch_rebases.go b/pkg/commands/patch_rebases.go new file mode 100644 index 000000000..40c3358ac --- /dev/null +++ b/pkg/commands/patch_rebases.go @@ -0,0 +1,153 @@ +package commands + +import "github.com/go-errors/errors" + +// DeletePatchesFromCommit applies a patch in reverse for a commit +func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error { + if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { + return err + } + + // apply each patch in reverse + if err := p.ApplyPatches(true); err != nil { + if err := c.GenericMerge("rebase", "abort"); err != nil { + return err + } + return err + } + + // time to amend the selected commit + if _, err := c.AmendHead(); err != nil { + return err + } + + // continue + return c.GenericMerge("rebase", "continue") +} + +func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error { + if sourceCommitIdx < destinationCommitIdx { + if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil { + return err + } + + // apply each patch forward + if err := p.ApplyPatches(false); err != nil { + if err := c.GenericMerge("rebase", "abort"); err != nil { + return err + } + return err + } + + // amend the destination commit + if _, err := c.AmendHead(); err != nil { + return err + } + + // continue + return c.GenericMerge("rebase", "continue") + } + + if len(commits)-1 < sourceCommitIdx { + return errors.New("index outside of range of commits") + } + + // we can make this GPG thing possible it just means we need to do this in two parts: + // one where we handle the possibility of a credential request, and the other + // where we continue the rebase + if c.usingGpg() { + return errors.New(c.Tr.SLocalize("DisabledForGPG")) + } + + baseIndex := sourceCommitIdx + 1 + todo := "" + for i, commit := range commits[0:baseIndex] { + a := "pick" + if i == sourceCommitIdx || i == destinationCommitIdx { + a = "edit" + } + todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo + } + + cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true) + if err != nil { + return err + } + + if err := c.OSCommand.RunPreparedCommand(cmd); err != nil { + return err + } + + // apply each patch in reverse + if err := p.ApplyPatches(true); err != nil { + if err := c.GenericMerge("rebase", "abort"); err != nil { + return err + } + return err + } + + // amend the source commit + if _, err := c.AmendHead(); err != nil { + return err + } + + if c.onSuccessfulContinue != nil { + return errors.New("You are midway through another rebase operation. Please abort to start again") + } + + c.onSuccessfulContinue = func() error { + // now we should be up to the destination, so let's apply forward these patches to that. + // ideally we would ensure we're on the right commit but I'm not sure if that check is necessary + if err := p.ApplyPatches(false); err != nil { + if err := c.GenericMerge("rebase", "abort"); err != nil { + return err + } + return err + } + + // amend the destination commit + if _, err := c.AmendHead(); err != nil { + return err + } + + return c.GenericMerge("rebase", "continue") + } + + return c.GenericMerge("rebase", "continue") +} + +func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error { + if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { + return err + } + + if err := p.ApplyPatches(true); err != nil { + if err := c.GenericMerge("rebase", "abort"); err != nil { + return err + } + return err + } + + // amend the commit + if _, err := c.AmendHead(); err != nil { + return err + } + + if c.onSuccessfulContinue != nil { + return errors.New("You are midway through another rebase operation. Please abort to start again") + } + + c.onSuccessfulContinue = func() error { + // add patches to index + if err := p.ApplyPatches(false); err != nil { + if err := c.GenericMerge("rebase", "abort"); err != nil { + return err + } + return err + } + + return nil + } + + return c.GenericMerge("rebase", "continue") +} diff --git a/pkg/git/testdata/addedFile.diff b/pkg/git/testdata/addedFile.diff deleted file mode 100644 index 53966c4a1..000000000 --- a/pkg/git/testdata/addedFile.diff +++ /dev/null @@ -1,7 +0,0 @@ -diff --git a/blah b/blah -new file mode 100644 -index 0000000..907b308 ---- /dev/null -+++ b/blah -@@ -0,0 +1 @@ -+blah diff --git a/pkg/git/testdata/testPatchAfter1.diff b/pkg/git/testdata/testPatchAfter1.diff deleted file mode 100644 index 88066e1c2..000000000 --- a/pkg/git/testdata/testPatchAfter1.diff +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go -index 60ec4e0..db4485d 100644 ---- a/pkg/git/branch_list_builder.go -+++ b/pkg/git/branch_list_builder.go -@@ -14,8 +14,7 @@ import ( - - // context: - // we want to only show 'safe' branches (ones that haven't e.g. been deleted) --// which `git branch -a` gives us, but we also want the recency data that - // git reflog gives us. - // So we get the HEAD, then append get the reflog branches that intersect with - // our safe branches, then add the remaining safe branches, ensuring uniqueness - // along the way diff --git a/pkg/git/testdata/testPatchAfter2.diff b/pkg/git/testdata/testPatchAfter2.diff deleted file mode 100644 index 0a17c2b67..000000000 --- a/pkg/git/testdata/testPatchAfter2.diff +++ /dev/null @@ -1,14 +0,0 @@ -diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go -index 60ec4e0..db4485d 100644 ---- a/pkg/git/branch_list_builder.go -+++ b/pkg/git/branch_list_builder.go -@@ -14,8 +14,9 @@ import ( - - // context: - // we want to only show 'safe' branches (ones that haven't e.g. been deleted) - // which `git branch -a` gives us, but we also want the recency data that - // git reflog gives us. -+// test 2 - if I remove this, I decrement the end counter - // So we get the HEAD, then append get the reflog branches that intersect with - // our safe branches, then add the remaining safe branches, ensuring uniqueness - // along the way diff --git a/pkg/git/testdata/testPatchAfter3.diff b/pkg/git/testdata/testPatchAfter3.diff deleted file mode 100644 index 03492450d..000000000 --- a/pkg/git/testdata/testPatchAfter3.diff +++ /dev/null @@ -1,25 +0,0 @@ -diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go -index a8fc600..6d8f7d7 100644 ---- a/pkg/git/patch_modifier.go -+++ b/pkg/git/patch_modifier.go -@@ -36,18 +36,19 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre - hunkEnd = hunkStarts[nextHunkStartIndex] - } - - headerLength := 4 - output := strings.Join(lines[0:headerLength], "\n") + "\n" - output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n" - - return output, nil - } - -+func getHeaderLength(patchLines []string) (int, error) { - // ModifyPatchForLine takes the original patch, which may contain several hunks, - // and the line number of the line we want to stage - func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) { - lines := strings.Split(patch, "\n") - headerLength := 4 - output := strings.Join(lines[0:headerLength], "\n") + "\n" - - hunkStart, err := p.getHunkStart(lines, lineNumber) - diff --git a/pkg/git/testdata/testPatchAfter4.diff b/pkg/git/testdata/testPatchAfter4.diff deleted file mode 100644 index 99f894d9d..000000000 --- a/pkg/git/testdata/testPatchAfter4.diff +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go -index a8fc600..6d8f7d7 100644 ---- a/pkg/git/patch_modifier.go -+++ b/pkg/git/patch_modifier.go -@@ -124,13 +140,14 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line - // @@ -14,8 +14,9 @@ import ( - func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) { - // current counter is the number after the second comma - re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`) - matches := re.FindStringSubmatch(currentHeader) - if len(matches) < 2 { - re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`) - matches = re.FindStringSubmatch(currentHeader) - } - prevLengthString := matches[1] -+ prevLengthString := re.FindStringSubmatch(currentHeader)[1] - - prevLength, err := strconv.Atoi(prevLengthString) - if err != nil { diff --git a/pkg/git/testdata/testPatchBefore.diff b/pkg/git/testdata/testPatchBefore.diff deleted file mode 100644 index 14e4b0e23..000000000 --- a/pkg/git/testdata/testPatchBefore.diff +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go -index 60ec4e0..db4485d 100644 ---- a/pkg/git/branch_list_builder.go -+++ b/pkg/git/branch_list_builder.go -@@ -14,8 +14,8 @@ import ( - - // context: - // we want to only show 'safe' branches (ones that haven't e.g. been deleted) --// which `git branch -a` gives us, but we also want the recency data that --// git reflog gives us. -+// test 2 - if I remove this, I decrement the end counter -+// test - // So we get the HEAD, then append get the reflog branches that intersect with - // our safe branches, then add the remaining safe branches, ensuring uniqueness - // along the way diff --git a/pkg/git/testdata/testPatchBefore2.diff b/pkg/git/testdata/testPatchBefore2.diff deleted file mode 100644 index 552c04f5e..000000000 --- a/pkg/git/testdata/testPatchBefore2.diff +++ /dev/null @@ -1,57 +0,0 @@ -diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go -index a8fc600..6d8f7d7 100644 ---- a/pkg/git/patch_modifier.go -+++ b/pkg/git/patch_modifier.go -@@ -36,18 +36,34 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre - hunkEnd = hunkStarts[nextHunkStartIndex] - } - -- headerLength := 4 -+ headerLength, err := getHeaderLength(lines) -+ if err != nil { -+ return "", err -+ } -+ - output := strings.Join(lines[0:headerLength], "\n") + "\n" - output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n" - - return output, nil - } - -+func getHeaderLength(patchLines []string) (int, error) { -+ for index, line := range patchLines { -+ if strings.HasPrefix(line, "@@") { -+ return index, nil -+ } -+ } -+ return 0, errors.New("Could not find any hunks in this patch") -+} -+ - // ModifyPatchForLine takes the original patch, which may contain several hunks, - // and the line number of the line we want to stage - func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) { - lines := strings.Split(patch, "\n") -- headerLength := 4 -+ headerLength, err := getHeaderLength(lines) -+ if err != nil { -+ return "", err -+ } - output := strings.Join(lines[0:headerLength], "\n") + "\n" - - hunkStart, err := p.getHunkStart(lines, lineNumber) -@@ -124,13 +140,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line - // @@ -14,8 +14,9 @@ import ( - func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) { - // current counter is the number after the second comma -- re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`) -- matches := re.FindStringSubmatch(currentHeader) -- if len(matches) < 2 { -- re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`) -- matches = re.FindStringSubmatch(currentHeader) -- } -- prevLengthString := matches[1] -+ re := regexp.MustCompile(`(\d+) @@`) -+ prevLengthString := re.FindStringSubmatch(currentHeader)[1] - - prevLength, err := strconv.Atoi(prevLengthString) - if err != nil { diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index f79dce905..ce9ac4daa 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -6,7 +6,6 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/git" ) // list panel functions @@ -67,7 +66,7 @@ func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error { // be sure there is a state.Branches array to pick the current branch from func (gui *Gui) refreshBranches(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error { - builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand) + builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand) if err != nil { return err } diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index 2cdda6815..a8e5b2f4f 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -1,6 +1,7 @@ package gui import ( + "github.com/go-errors/errors" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" ) @@ -23,7 +24,7 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error { if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil { return err } - commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name) + commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, false) if err != nil { return err } @@ -79,16 +80,19 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) refreshCommitFilesView() error { + if err := gui.refreshPatchPanel(); err != nil { + return err + } + commit := gui.getSelectedCommit(gui.g) if commit == nil { return nil } - files, err := gui.GitCommand.GetCommitFiles(commit.Sha) + files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.State.PatchManager) if err != nil { return gui.createErrorPanel(gui.g, err.Error()) } - gui.State.CommitFiles = files gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles)) @@ -104,3 +108,82 @@ func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error { file := gui.getSelectedCommitFile(g) return gui.openFile(file.Name) } + +func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error { + commitFile := gui.getSelectedCommitFile(g) + if commitFile == nil { + return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + } + + toggleTheFile := func() error { + if gui.State.PatchManager == nil { + if err := gui.createPatchManager(); err != nil { + return err + } + } + + gui.State.PatchManager.ToggleFileWhole(commitFile.Name) + + return gui.refreshCommitFilesView() + } + + if gui.State.PatchManager != nil && gui.State.PatchManager.CommitSha != commitFile.Sha { + return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error { + gui.State.PatchManager = nil + return toggleTheFile() + }, nil) + } + + return toggleTheFile() +} + +func (gui *Gui) createPatchManager() error { + diffMap := map[string]string{} + for _, commitFile := range gui.State.CommitFiles { + commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true) + if err != nil { + return err + } + diffMap[commitFile.Name] = commitText + } + + commit := gui.getSelectedCommit(gui.g) + if commit == nil { + return errors.New("No commit selected") + } + + gui.State.PatchManager = commands.NewPatchManager(gui.Log, gui.GitCommand.ApplyPatch, commit.Sha, diffMap) + return nil +} + +func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error { + commitFile := gui.getSelectedCommitFile(g) + if commitFile == nil { + return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + } + + enterTheFile := func() error { + if gui.State.PatchManager == nil { + if err := gui.createPatchManager(); err != nil { + return err + } + } + + if err := gui.changeContext("main", "staging"); err != nil { + return err + } + if err := gui.switchFocus(g, v, gui.getMainView()); err != nil { + return err + } + return gui.refreshStagingPanel() + } + + if gui.State.PatchManager != nil && gui.State.PatchManager.CommitSha != commitFile.Sha { + return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error { + gui.State.PatchManager = nil + return enterTheFile() + }, nil) + } + + return enterTheFile() +} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index c35b4adee..b22e6084e 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -9,7 +9,6 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/git" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -53,9 +52,26 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { return gui.renderString(g, "main", commitText) } +func (gui *Gui) refreshPatchPanel() error { + if gui.State.PatchManager != nil { + gui.State.SplitMainPanel = true + secondaryView := gui.getSecondaryView() + secondaryView.Highlight = true + secondaryView.Wrap = false + + gui.g.Update(func(*gocui.Gui) error { + return gui.setViewContent(gui.g, gui.getSecondaryView(), gui.State.PatchManager.RenderAggregatedPatchColored(false)) + }) + } else { + gui.State.SplitMainPanel = false + } + + return nil +} + func (gui *Gui) refreshCommits(g *gocui.Gui) error { g.Update(func(*gocui.Gui) error { - builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries) + builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries) if err != nil { return err } @@ -65,6 +81,10 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error { } gui.State.Commits = commits + if err := gui.refreshPatchPanel(); err != nil { + return err + } + gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits)) isFocused := gui.g.CurrentView().Name() == "commits" diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index e66df31d8..ea71d9dca 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -23,7 +23,6 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/git" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/updates" @@ -84,13 +83,13 @@ type Gui struct { // non-mutative, so that we don't accidentally end up // with mismatches of data. We might change this in the future type stagingPanelState struct { - SelectedLineIdx int - FirstLineIdx int - LastLineIdx int - Diff string - PatchParser *git.PatchParser - SelectMode int // one of LINE, HUNK, or RANGE - IndexFocused bool // this is for if we show the left or right panel + SelectedLineIdx int + FirstLineIdx int + LastLineIdx int + Diff string + PatchParser *commands.PatchParser + SelectMode int // one of LINE, HUNK, or RANGE + SecondaryFocused bool // this is for if we show the left or right panel } type mergingPanelState struct { @@ -152,8 +151,11 @@ type guiState struct { Contexts map[string]string CherryPickedCommits []*commands.Commit SplitMainPanel bool + PatchManager *commands.PatchManager } +// for now the split view will always be on + // NewGui builds a new gui handler func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) { @@ -390,7 +392,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { main := "main" secondary := "secondary" - swappingMainPanels := gui.State.Panels.Staging != nil && gui.State.Panels.Staging.IndexFocused + swappingMainPanels := gui.State.Panels.Staging != nil && gui.State.Panels.Staging.SecondaryFocused if swappingMainPanels { main = "secondary" secondary = "main" diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 77d295d33..7718535ed 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -142,6 +142,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Key: 'x', Modifier: gocui.ModNone, Handler: gui.handleCreateOptionsMenu, + }, { + ViewName: "", + Key: gocui.KeyCtrlP, + Modifier: gocui.ModNone, + Handler: gui.handleCreatePatchOptionsMenu, }, { ViewName: "status", Key: 'e', @@ -523,6 +528,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleOpenOldCommitFile, Description: gui.Tr.SLocalize("openFile"), }, + { + ViewName: "commitFiles", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleToggleFileForPatch, + Description: gui.Tr.SLocalize("toggleAddToPatch"), + }, + { + ViewName: "commitFiles", + Key: gocui.KeyEnter, + Modifier: gocui.ModNone, + Handler: gui.handleEnterCommitFile, + Description: gui.Tr.SLocalize("enterFile"), + }, } for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} { diff --git a/pkg/gui/patch_options_panel.go b/pkg/gui/patch_options_panel.go new file mode 100644 index 000000000..14d13cd82 --- /dev/null +++ b/pkg/gui/patch_options_panel.go @@ -0,0 +1,89 @@ +package gui + +import ( + "fmt" + + "github.com/jesseduffield/gocui" +) + +type patchMenuOption struct { + displayName string + function func() error +} + +// GetDisplayStrings is a function. +func (o *patchMenuOption) GetDisplayStrings(isFocused bool) []string { + return []string{o.displayName} +} + +func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error { + m := gui.State.PatchManager + if m == nil { + return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError")) + } + + options := []*patchMenuOption{ + {displayName: "discard patch", function: gui.handleDeletePatchFromCommit}, + {displayName: "pull patch out into index", function: gui.handlePullPatchIntoWorkingTree}, + {displayName: "save patch to file"}, + {displayName: "clear patch", function: gui.handleClearPatch}, + } + + selectedCommit := gui.getSelectedCommit(gui.g) + if selectedCommit != nil && gui.State.PatchManager.CommitSha != selectedCommit.Sha { + options = append(options, &patchMenuOption{ + displayName: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha), + function: gui.handleMovePatchToSelectedCommit, + }) + } + + handleMenuPress := func(index int) error { + return options[index].function() + } + + return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), options, len(options), handleMenuPress) +} + +func (gui *Gui) getPatchCommitIndex() int { + for index, commit := range gui.State.Commits { + if commit.Sha == gui.State.PatchManager.CommitSha { + return index + } + } + return -1 +} + +func (gui *Gui) handleDeletePatchFromCommit() error { + // TODO: deal with when we're already rebasing + + return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error { + commitIndex := gui.getPatchCommitIndex() + err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.State.PatchManager) + return gui.handleGenericMergeCommandResult(err) + }) +} + +func (gui *Gui) handleMovePatchToSelectedCommit() error { + // TODO: deal with when we're already rebasing + + return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error { + commitIndex := gui.getPatchCommitIndex() + err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLine, gui.State.PatchManager) + return gui.handleGenericMergeCommandResult(err) + }) +} + +func (gui *Gui) handlePullPatchIntoWorkingTree() error { + // TODO: deal with when we're already rebasing + + return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error { + commitIndex := gui.getPatchCommitIndex() + err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.State.PatchManager) + return gui.handleGenericMergeCommandResult(err) + }) +} + +func (gui *Gui) handleClearPatch() error { + gui.State.PatchManager = nil + return gui.refreshCommitFilesView() +} diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index 93e1d46c5..0a47d0a76 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -1,12 +1,11 @@ package gui import ( - "strings" - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/git" + "github.com/jesseduffield/lazygit/pkg/commands" ) +// these represent what select mode we're in const ( LINE = iota RANGE @@ -16,49 +15,65 @@ const ( func (gui *Gui) refreshStagingPanel() error { state := gui.State.Panels.Staging - file, err := gui.getSelectedFile(gui.g) - if err != nil { - if err != gui.Errors.ErrNoFiles { - return err - } - return gui.handleStagingEscape(gui.g, nil) - } + // file, err := gui.getSelectedFile(gui.g) + // if err != nil { + // if err != gui.Errors.ErrNoFiles { + // return err + // } + // return gui.handleStagingEscape(gui.g, nil) + // } gui.State.SplitMainPanel = true - indexFocused := false + secondaryFocused := false if state != nil { - indexFocused = state.IndexFocused + secondaryFocused = state.SecondaryFocused } - if !file.HasUnstagedChanges && !file.HasStagedChanges { - return gui.handleStagingEscape(gui.g, nil) + // if !file.HasUnstagedChanges && !file.HasStagedChanges { + // return gui.handleStagingEscape(gui.g, nil) + // } + + // if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) { + // secondaryFocused = !secondaryFocused + // } + + // getDiffs := func() (string, string) { + // // note for custom diffs, we'll need to send a flag here saying not to use the custom diff + // diff := gui.GitCommand.Diff(file, true, secondaryFocused) + // secondaryColorDiff := gui.GitCommand.Diff(file, false, !secondaryFocused) + // return diff, secondaryColorDiff + // } + + // diff, secondaryColorDiff := getDiffs() + + // // if we have e.g. a deleted file with nothing else to the diff will have only + // // 4-5 lines in which case we'll swap panels + // if len(strings.Split(diff, "\n")) < 5 { + // if len(strings.Split(secondaryColorDiff, "\n")) < 5 { + // return gui.handleStagingEscape(gui.g, nil) + // } + // secondaryFocused = !secondaryFocused + // diff, secondaryColorDiff = getDiffs() + // } + + // get diff from commit file that's currently selected + commitFile := gui.getSelectedCommitFile(gui.g) + if commitFile == nil { + return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) } - if (indexFocused && !file.HasStagedChanges) || (!indexFocused && !file.HasUnstagedChanges) { - indexFocused = !indexFocused + diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true) + if err != nil { + return err } - getDiffs := func() (string, string) { - // note for custom diffs, we'll need to send a flag here saying not to use the custom diff - diff := gui.GitCommand.Diff(file, true, indexFocused) - secondaryColorDiff := gui.GitCommand.Diff(file, false, !indexFocused) - return diff, secondaryColorDiff + secondaryColorDiff := gui.State.PatchManager.RenderPatchForFile(commitFile.Name, false, false) + if err != nil { + return err } - diff, secondaryColorDiff := getDiffs() - - // if we have e.g. a deleted file with nothing else to the diff will have only - // 4-5 lines in which case we'll swap panels - if len(strings.Split(diff, "\n")) < 5 { - if len(strings.Split(secondaryColorDiff, "\n")) < 5 { - return gui.handleStagingEscape(gui.g, nil) - } - indexFocused = !indexFocused - diff, secondaryColorDiff = getDiffs() - } - - patchParser, err := git.NewPatchParser(gui.Log, diff) + patchParser, err := commands.NewPatchParser(gui.Log, diff) if err != nil { return nil } @@ -92,13 +107,13 @@ func (gui *Gui) refreshStagingPanel() error { } gui.State.Panels.Staging = &stagingPanelState{ - PatchParser: patchParser, - SelectedLineIdx: selectedLineIdx, - SelectMode: selectMode, - FirstLineIdx: firstLineIdx, - LastLineIdx: lastLineIdx, - Diff: diff, - IndexFocused: indexFocused, + PatchParser: patchParser, + SelectedLineIdx: selectedLineIdx, + SelectMode: selectMode, + FirstLineIdx: firstLineIdx, + LastLineIdx: lastLineIdx, + Diff: diff, + SecondaryFocused: secondaryFocused, } if err := gui.refreshView(); err != nil { @@ -123,14 +138,14 @@ func (gui *Gui) refreshStagingPanel() error { func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error { state := gui.State.Panels.Staging - state.IndexFocused = !state.IndexFocused + state.SecondaryFocused = !state.SecondaryFocused return gui.refreshStagingPanel() } func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Staging = nil - return gui.switchFocus(gui.g, nil, gui.getFilesView()) + return gui.switchFocus(gui.g, nil, gui.getCommitFilesView()) } func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error { @@ -203,7 +218,9 @@ func (gui *Gui) handleCycleLine(change int) error { func (gui *Gui) refreshView() error { state := gui.State.Panels.Staging - colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx) + filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name + + colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, gui.State.PatchManager.GetFileIncLineIndices(filename)) mainView := gui.getMainView() mainView.Highlight = true @@ -258,17 +275,57 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool) error { } func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error { - return gui.applySelection(false) + state := gui.State.Panels.Staging + + // add range of lines to those set for the file + commitFile := gui.getSelectedCommitFile(gui.g) + if commitFile == nil { + return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + } + + gui.State.PatchManager.AddFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx) + + if err := gui.refreshCommitFilesView(); err != nil { + return err + } + + if err := gui.refreshStagingPanel(); err != nil { + return err + } + + return nil + + // return gui.applySelection(false) } func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error { - return gui.applySelection(true) + state := gui.State.Panels.Staging + + // add range of lines to those set for the file + commitFile := gui.getSelectedCommitFile(gui.g) + if commitFile == nil { + return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + } + + gui.State.PatchManager.RemoveFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx) + + if err := gui.refreshCommitFilesView(); err != nil { + return err + } + + if err := gui.refreshStagingPanel(); err != nil { + return err + } + + return nil + + // return gui.applySelection(true) } func (gui *Gui) applySelection(reverse bool) error { state := gui.State.Panels.Staging - if !reverse && state.IndexFocused { + if !reverse && state.SecondaryFocused { return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged")) } @@ -277,7 +334,7 @@ func (gui *Gui) applySelection(reverse bool) error { return err } - patch := git.ModifiedPatch(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse) + patch := commands.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false) if patch == "" { return nil @@ -285,7 +342,7 @@ func (gui *Gui) applySelection(reverse bool) error { // apply the patch then refresh this panel // create a new temp file with the patch, then call git apply with that patch - _, err = gui.GitCommand.ApplyPatch(patch, false, !reverse || state.IndexFocused) + err = gui.GitCommand.ApplyPatch(patch, false, !reverse || state.SecondaryFocused, "") if err != nil { return err } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 5c5678a14..719285be3 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -368,13 +368,13 @@ func (gui *Gui) changeSelectedLine(line *int, total int, up bool) { return } - *line -= 1 + *line-- } else { if *line == -1 || *line == total-1 { return } - *line += 1 + *line++ } } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index c250dcbd4..d5f46ac2d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -794,6 +794,24 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "jump", Other: "jump to panel", + }, &i18n.Message{ + ID: "DiscardPatch", + Other: "Discard Patch", + }, &i18n.Message{ + ID: "DiscardPatchConfirm", + Other: "You can only build a patch from one commit at a time. Discard current patch?", + }, &i18n.Message{ + ID: "toggleAddToPatch", + Other: "toggle file included in patch", + }, &i18n.Message{ + ID: "PatchOptionsTitle", + Other: "Patch Options", + }, &i18n.Message{ + ID: "NoPatchError", + Other: "No patch created yet. To start building a patch, use 'space' on a commit file or enter to add specific lines", + }, &i18n.Message{ + ID: "enterFile", + Other: "enter file to add selected lines to the patch", }, ) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index f184c0bbb..bf69fd30e 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -261,3 +261,41 @@ func AsJson(i interface{}) string { bytes, _ := json.MarshalIndent(i, "", " ") return string(bytes) } + +// UnionInt returns the union of two int arrays +func UnionInt(a, b []int) []int { + m := make(map[int]bool) + + for _, item := range a { + m[item] = true + } + + for _, item := range b { + if _, ok := m[item]; !ok { + // this does not mutate the original a slice + // though it does mutate the backing array I believe + // but that doesn't matter because if you later want to append to the + // original a it must see that the backing array has been changed + // and create a new one + a = append(a, item) + } + } + return a +} + +// DifferenceInt returns the difference of two int arrays +func DifferenceInt(a, b []int) []int { + result := []int{} + m := make(map[int]bool) + + for _, item := range b { + m[item] = true + } + + for _, item := range a { + if _, ok := m[item]; !ok { + result = append(result, item) + } + } + return result +}