1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-06 03:53:59 +02:00

support cherry picking commits

This commit is contained in:
Jesse Duffield Duffield 2019-02-24 13:51:52 +11:00
parent 1a19b1412d
commit a8858cbd12
7 changed files with 169 additions and 18 deletions

View File

@ -12,6 +12,7 @@ type Commit struct {
Status string // one of "unpushed", "pushed", "merged", or "rebasing" Status string // one of "unpushed", "pushed", "merged", or "rebasing"
DisplayString string DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup" 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. // GetDisplayStrings is a function.
@ -19,9 +20,14 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
red := color.New(color.FgRed) red := color.New(color.FgRed)
yellow := color.New(color.FgYellow) yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen) green := color.New(color.FgGreen)
white := color.New(color.FgWhite)
blue := color.New(color.FgBlue) blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan) 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 var shaColor *color.Color
switch c.Status { switch c.Status {
@ -37,6 +43,10 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
shaColor = white shaColor = white
} }
if c.Copied {
shaColor = copied
}
actionString := "" actionString := ""
if c.Action != "" { if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " " actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "

View File

@ -745,3 +745,18 @@ func (c *GitCommand) MoveTodoDown(index int) error {
func (c *GitCommand) Revert(sha string) error { func (c *GitCommand) Revert(sha string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha)) 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)
}

View File

@ -23,19 +23,21 @@ import (
// CommitListBuilder returns a list of Branch objects for the current repo // CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct { type CommitListBuilder struct {
Log *logrus.Entry Log *logrus.Entry
GitCommand *commands.GitCommand GitCommand *commands.GitCommand
OSCommand *commands.OSCommand OSCommand *commands.OSCommand
Tr *i18n.Localizer Tr *i18n.Localizer
CherryPickedShas []string
} }
// NewCommitListBuilder builds a new commit list builder // 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{ return &CommitListBuilder{
Log: log, Log: log,
GitCommand: gitCommand, GitCommand: gitCommand,
OSCommand: osCommand, OSCommand: osCommand,
Tr: tr, Tr: tr,
CherryPickedShas: cherryPickedShas,
}, nil }, nil
} }
@ -80,7 +82,18 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere")) youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere"))
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name) 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: // git-rebase-todo example:
@ -106,7 +119,7 @@ func (c *CommitListBuilder) getRebasingCommits() ([]*commands.Commit, error) {
commits := []*commands.Commit{} commits := []*commands.Commit{}
lines := strings.Split(string(bytesContent), "\n") lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines { for _, line := range lines {
if line == "" { if line == "" || line == "noop" {
return commits, nil return commits, nil
} }
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
@ -144,6 +157,17 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit)
return commits, nil 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) { func (c *CommitListBuilder) getMergeBase() (string, error) {
currentBranch, err := c.GitCommand.CurrentBranchName() currentBranch, err := c.GitCommand.CurrentBranchName()
if err != nil { if err != nil {

View File

@ -2,6 +2,8 @@ package gui
import ( import (
"fmt" "fmt"
"strconv"
"strings"
"github.com/go-errors/errors" "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 { func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*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 { if err != nil {
return err return err
} }
@ -347,3 +349,67 @@ func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Commits.SelectedLine++ gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g) 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)
}

View File

@ -133,17 +133,19 @@ type guiState struct {
Panels *panelStates Panels *panelStates
WorkingTreeState string // one of "merging", "rebasing", "normal" WorkingTreeState string // one of "merging", "rebasing", "normal"
Contexts map[string]string Contexts map[string]string
CherryPickedShas []string
} }
// NewGui builds a new gui handler // 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) { func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
initialState := guiState{ initialState := guiState{
Files: make([]*commands.File, 0), Files: make([]*commands.File, 0),
PreviousView: "files", PreviousView: "files",
Commits: make([]*commands.Commit, 0), Commits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0), CherryPickedShas: []string{},
Platform: *oSCommand.Platform, StashEntries: make([]*commands.StashEntry, 0),
Platform: *oSCommand.Platform,
Panels: &panelStates{ Panels: &panelStates{
Files: &filePanelState{SelectedLine: -1}, Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0}, Branches: &branchPanelState{SelectedLine: 0},

View File

@ -358,6 +358,24 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleCommitRevert, Handler: gui.handleCommitRevert,
Description: gui.Tr.SLocalize("revertCommit"), 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", ViewName: "stash",
Key: gocui.KeySpace, Key: gocui.KeySpace,
@ -365,6 +383,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleStashApply, Handler: gui.handleStashApply,
Description: gui.Tr.SLocalize("apply"), Description: gui.Tr.SLocalize("apply"),
}, { }, {
ViewName: "stash", ViewName: "stash",
Key: 'g', Key: 'g',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,

View File

@ -571,6 +571,21 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "rewordNotSupported", ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported", 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",
}, },
) )
} }