diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go index 1df6afbd0..4253a5495 100644 --- a/pkg/commands/commit.go +++ b/pkg/commands/commit.go @@ -12,6 +12,7 @@ type Commit struct { Status string // one of "unpushed", "pushed", "merged", or "rebasing" DisplayString string Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup" + Copied bool // to know if this commit is ready to be cherry-picked somewhere } // GetDisplayStrings is a function. @@ -19,9 +20,14 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string { red := color.New(color.FgRed) yellow := color.New(color.FgYellow) green := color.New(color.FgGreen) - white := color.New(color.FgWhite) blue := color.New(color.FgBlue) cyan := color.New(color.FgCyan) + white := color.New(color.FgWhite) + + // for some reason, setting the background to blue pads out the other commits + // horizontally. For the sake of accessibility I'm considering this a feature, + // not a bug + copied := color.New(color.FgCyan, color.BgBlue) var shaColor *color.Color switch c.Status { @@ -37,6 +43,10 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string { shaColor = white } + if c.Copied { + shaColor = copied + } + actionString := "" if c.Action != "" { actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " " diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 7b2c2f2fa..7127a427e 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -745,3 +745,18 @@ func (c *GitCommand) MoveTodoDown(index int) error { func (c *GitCommand) Revert(sha string) error { return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha)) } + +// CherryPickShas begins an interactive rebase with the given shas being cherry picked onto HEAD +func (c *GitCommand) CherryPickShas(shas []string) error { + todo := "" + for _, sha := range shas { + todo = "pick " + sha + "\n" + todo + } + + cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false) + if err != nil { + return err + } + + return c.OSCommand.RunPreparedCommand(cmd) +} diff --git a/pkg/git/commit_list_builder.go b/pkg/git/commit_list_builder.go index ffb97c53b..3de255d02 100644 --- a/pkg/git/commit_list_builder.go +++ b/pkg/git/commit_list_builder.go @@ -23,19 +23,21 @@ import ( // CommitListBuilder returns a list of Branch objects for the current repo type CommitListBuilder struct { - Log *logrus.Entry - GitCommand *commands.GitCommand - OSCommand *commands.OSCommand - Tr *i18n.Localizer + Log *logrus.Entry + GitCommand *commands.GitCommand + OSCommand *commands.OSCommand + Tr *i18n.Localizer + CherryPickedShas []string } // NewCommitListBuilder builds a new commit list builder -func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer) (*CommitListBuilder, error) { +func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedShas []string) (*CommitListBuilder, error) { return &CommitListBuilder{ - Log: log, - GitCommand: gitCommand, - OSCommand: osCommand, - Tr: tr, + Log: log, + GitCommand: gitCommand, + OSCommand: osCommand, + Tr: tr, + CherryPickedShas: cherryPickedShas, }, nil } @@ -80,7 +82,18 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere")) currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name) } - return c.setCommitMergedStatuses(commits) + + commits, err = c.setCommitMergedStatuses(commits) + if err != nil { + return nil, err + } + + commits, err = c.setCommitCherryPickStatuses(commits) + if err != nil { + return nil, err + } + + return commits, nil } // git-rebase-todo example: @@ -106,7 +119,7 @@ func (c *CommitListBuilder) getRebasingCommits() ([]*commands.Commit, error) { commits := []*commands.Commit{} lines := strings.Split(string(bytesContent), "\n") for _, line := range lines { - if line == "" { + if line == "" || line == "noop" { return commits, nil } splitLine := strings.Split(line, " ") @@ -144,6 +157,17 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) return commits, nil } +func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) { + for _, commit := range commits { + for _, sha := range c.CherryPickedShas { + if commit.Sha == sha { + commit.Copied = true + } + } + } + return commits, nil +} + func (c *CommitListBuilder) getMergeBase() (string, error) { currentBranch, err := c.GitCommand.CurrentBranchName() if err != nil { diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 383f9b6ba..37b7538f7 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -2,6 +2,8 @@ package gui import ( "fmt" + "strconv" + "strings" "github.com/go-errors/errors" @@ -40,7 +42,7 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { 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) + builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedShas) if err != nil { return err } @@ -347,3 +349,67 @@ func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Commits.SelectedLine++ return gui.refreshCommits(gui.g) } + +func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error { + // get currently selected commit, add the sha to state. + sha := gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha + + // we will un-copy it if it's already copied + for index, cherryPickedSha := range gui.State.CherryPickedShas { + if sha == cherryPickedSha { + gui.State.CherryPickedShas = append(gui.State.CherryPickedShas[0:index], gui.State.CherryPickedShas[index+1:]...) + gui.Log.Info("removed copied sha. New shas:\n" + strings.Join(gui.State.CherryPickedShas, "\n")) + return gui.refreshCommits(gui.g) + } + } + + gui.addCommitToCherryPickedShas(gui.State.Panels.Commits.SelectedLine) + return gui.refreshCommits(gui.g) +} + +func (gui *Gui) addCommitToCherryPickedShas(index int) { + defer func() { gui.Log.Info("new copied shas:\n" + strings.Join(gui.State.CherryPickedShas, "\n")) }() + + // not super happy with modifying the state of the Commits array here + // but the alternative would be very tricky + gui.State.Commits[index].Copied = true + + newShas := []string{} + for _, commit := range gui.State.Commits { + if commit.Copied { + newShas = append(newShas, commit.Sha) + } + } + + gui.State.CherryPickedShas = newShas +} + +func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error { + // whenever I add a commit, I need to make sure I retain its order + + // find the last commit that is copied that's above our position + // if there are none, startIndex = 0 + startIndex := 0 + for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] { + if commit.Copied { + startIndex = index + } + } + + gui.Log.Info("commit copy start index: " + strconv.Itoa(startIndex)) + + for index := startIndex; index <= gui.State.Panels.Commits.SelectedLine; index++ { + gui.addCommitToCherryPickedShas(index) + } + + return gui.refreshCommits(gui.g) +} + +// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied +func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error { + err := gui.GitCommand.CherryPickShas(gui.State.CherryPickedShas) + return gui.handleGenericMergeCommandResult(err) + }, nil) + +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 11daec652..2103856fd 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -133,17 +133,19 @@ type guiState struct { Panels *panelStates WorkingTreeState string // one of "merging", "rebasing", "normal" Contexts map[string]string + CherryPickedShas []string } // 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) { initialState := guiState{ - Files: make([]*commands.File, 0), - PreviousView: "files", - Commits: make([]*commands.Commit, 0), - StashEntries: make([]*commands.StashEntry, 0), - Platform: *oSCommand.Platform, + Files: make([]*commands.File, 0), + PreviousView: "files", + Commits: make([]*commands.Commit, 0), + CherryPickedShas: []string{}, + StashEntries: make([]*commands.StashEntry, 0), + Platform: *oSCommand.Platform, Panels: &panelStates{ Files: &filePanelState{SelectedLine: -1}, Branches: &branchPanelState{SelectedLine: 0}, diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index ab70616ea..3992513ef 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -358,6 +358,24 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleCommitRevert, Description: gui.Tr.SLocalize("revertCommit"), + }, { + ViewName: "commits", + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.handleCopyCommit, + Description: gui.Tr.SLocalize("cherryPickCopy"), + }, { + ViewName: "commits", + Key: 'C', + Modifier: gocui.ModNone, + Handler: gui.handleCopyCommitRange, + Description: gui.Tr.SLocalize("cherryPickCopyRange"), + }, { + ViewName: "commits", + Key: 'v', + Modifier: gocui.ModNone, + Handler: gui.HandlePasteCommits, + Description: gui.Tr.SLocalize("pasteCommits"), }, { ViewName: "stash", Key: gocui.KeySpace, @@ -365,6 +383,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleStashApply, Description: gui.Tr.SLocalize("apply"), }, { + ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index f302cd650..626d6d4cc 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -571,6 +571,21 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "rewordNotSupported", Other: "rewording commits while interactively rebasing is not currently supported", + }, &i18n.Message{ + ID: "cherryPickCopy", + Other: "copy commit (cherry-pick)", + }, &i18n.Message{ + ID: "cherryPickCopyRange", + Other: "copy commit range (cherry-pick)", + }, &i18n.Message{ + ID: "pasteCommits", + Other: "paste commits (cherry-pick)", + }, &i18n.Message{ + ID: "SureCherryPick", + Other: "Are you sure you want to cherry-pick the copied commits onto this branch?", + }, &i18n.Message{ + ID: "CherryPick", + Other: "Cherry-Pick", }, ) }