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:
parent
1a19b1412d
commit
a8858cbd12
@ -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)) + " "
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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},
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user