mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-03-21 21:47:32 +02:00
add stash panel
This commit is contained in:
parent
164ac72c5a
commit
d1ead5b0cf
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
|
|
||||||
// "log"
|
// "log"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -13,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GitFile : A staged/unstaged file
|
// GitFile : A staged/unstaged file
|
||||||
|
// TODO: decide whether to give all of these the Git prefix
|
||||||
type GitFile struct {
|
type GitFile struct {
|
||||||
Name string
|
Name string
|
||||||
HasStagedChanges bool
|
HasStagedChanges bool
|
||||||
@ -39,6 +41,13 @@ type Commit struct {
|
|||||||
DisplayString string
|
DisplayString string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StashEntry : A git stash entry
|
||||||
|
type StashEntry struct {
|
||||||
|
Index int
|
||||||
|
Name string
|
||||||
|
DisplayString string
|
||||||
|
}
|
||||||
|
|
||||||
func devLog(objects ...interface{}) {
|
func devLog(objects ...interface{}) {
|
||||||
localLog(color.FgWhite, "/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", objects...)
|
localLog(color.FgWhite, "/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", objects...)
|
||||||
}
|
}
|
||||||
@ -178,13 +187,34 @@ func getGitBranches() []Branch {
|
|||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
rawString, _ := runDirectCommand(getBranchesCommand)
|
rawString, _ := runDirectCommand(getBranchesCommand)
|
||||||
branchLines := splitLines(rawString)
|
for i, line := range splitLines(rawString) {
|
||||||
for i, line := range branchLines {
|
|
||||||
branches = append(branches, branchFromLine(line, i))
|
branches = append(branches, branchFromLine(line, i))
|
||||||
}
|
}
|
||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: DRY up this function and getGitBranches
|
||||||
|
func getGitStashEntries() []StashEntry {
|
||||||
|
stashEntries := make([]StashEntry, 0)
|
||||||
|
rawString, _ := runDirectCommand("git stash list --pretty='%gs'")
|
||||||
|
for i, line := range splitLines(rawString) {
|
||||||
|
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
||||||
|
}
|
||||||
|
return stashEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
func stashEntryFromLine(line string, index int) StashEntry {
|
||||||
|
return StashEntry{
|
||||||
|
Name: line,
|
||||||
|
Index: index,
|
||||||
|
DisplayString: line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStashEntryDiff(index int) (string, error) {
|
||||||
|
return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
|
||||||
|
}
|
||||||
|
|
||||||
func getGitStatusFiles() []GitFile {
|
func getGitStatusFiles() []GitFile {
|
||||||
statusOutput, _ := getGitStatus()
|
statusOutput, _ := getGitStatus()
|
||||||
statusStrings := splitLines(statusOutput)
|
statusStrings := splitLines(statusOutput)
|
||||||
@ -208,6 +238,23 @@ func getGitStatusFiles() []GitFile {
|
|||||||
return gitFiles
|
return gitFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gitStashDo(index int, method string) (string, error) {
|
||||||
|
return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitStashSave(message string) (string, error) {
|
||||||
|
output, err := runCommand("git stash save \"" + message + "\"")
|
||||||
|
if err != nil {
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
// if there are no local changes to save, the exit code is 0, but we want
|
||||||
|
// to raise an error
|
||||||
|
if output == "No local changes to save\n" {
|
||||||
|
return output, errors.New(output)
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
func gitCheckout(branch string, force bool) (string, error) {
|
func gitCheckout(branch string, force bool) (string, error) {
|
||||||
forceArg := ""
|
forceArg := ""
|
||||||
if force {
|
if force {
|
||||||
@ -369,6 +416,10 @@ func gitNewBranch(name string) (string, error) {
|
|||||||
return runDirectCommand("git checkout -b " + name)
|
return runDirectCommand("git checkout -b " + name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gitListStash() (string, error) {
|
||||||
|
return runDirectCommand("git stash list")
|
||||||
|
}
|
||||||
|
|
||||||
func gitUpstreamDifferenceCount() (string, string) {
|
func gitUpstreamDifferenceCount() (string, string) {
|
||||||
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
|
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -378,7 +429,7 @@ func gitUpstreamDifferenceCount() (string, string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "?", "?"
|
return "?", "?"
|
||||||
}
|
}
|
||||||
return strings.Trim(pushableCount, " \n"), strings.Trim(pullableCount, " \n")
|
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitCommitsToPush() []string {
|
func gitCommitsToPush() []string {
|
||||||
|
74
gui.go
74
gui.go
@ -15,6 +15,7 @@ type stateType struct {
|
|||||||
GitFiles []GitFile
|
GitFiles []GitFile
|
||||||
Branches []Branch
|
Branches []Branch
|
||||||
Commits []Commit
|
Commits []Commit
|
||||||
|
StashEntries []StashEntry
|
||||||
PreviousView string
|
PreviousView string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,9 +23,10 @@ var state = stateType{
|
|||||||
GitFiles: make([]GitFile, 0),
|
GitFiles: make([]GitFile, 0),
|
||||||
PreviousView: "files",
|
PreviousView: "files",
|
||||||
Commits: make([]Commit, 0),
|
Commits: make([]Commit, 0),
|
||||||
|
StashEntries: make([]StashEntry, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
var cyclableViews = []string{"files", "branches", "commits"}
|
var cyclableViews = []string{"files", "branches", "commits", "stash"}
|
||||||
|
|
||||||
func refreshSidePanels(g *gocui.Gui, v *gocui.View) error {
|
func refreshSidePanels(g *gocui.Gui, v *gocui.View) error {
|
||||||
refreshBranches(g)
|
refreshBranches(g)
|
||||||
@ -66,14 +68,14 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return handleFileSelect(g, v)
|
return handleFileSelect(g, v)
|
||||||
case "branches":
|
case "branches":
|
||||||
return handleBranchSelect(g, v)
|
return handleBranchSelect(g, v)
|
||||||
case "commit":
|
|
||||||
return handleCommitPromptFocus(g, v)
|
|
||||||
case "confirmation":
|
case "confirmation":
|
||||||
return nil
|
return nil
|
||||||
case "main":
|
case "main":
|
||||||
return nil
|
return nil
|
||||||
case "commits":
|
case "commits":
|
||||||
return handleCommitSelect(g, v)
|
return handleCommitSelect(g, v)
|
||||||
|
case "stash":
|
||||||
|
return handleStashEntrySelect(g, v)
|
||||||
default:
|
default:
|
||||||
panic("No view matching newLineFocused switch statement")
|
panic("No view matching newLineFocused switch statement")
|
||||||
}
|
}
|
||||||
@ -125,7 +127,7 @@ func keybindings(g *gocui.Gui) error {
|
|||||||
if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil {
|
if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("files", 'r', gocui.ModNone, handleFileRemove); err != nil {
|
if err := g.SetKeybinding("files", 'd', gocui.ModNone, handleFileRemove); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil {
|
if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil {
|
||||||
@ -143,10 +145,7 @@ func keybindings(g *gocui.Gui) error {
|
|||||||
if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil {
|
if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("commit", gocui.KeyEsc, gocui.ModNone, closeCommitPrompt); err != nil {
|
if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := g.SetKeybinding("commit", gocui.KeyEnter, gocui.ModNone, handleCommitSubmit); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil {
|
if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil {
|
||||||
@ -167,9 +166,21 @@ func keybindings(g *gocui.Gui) error {
|
|||||||
if err := g.SetKeybinding("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil {
|
if err := g.SetKeybinding("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("", 'S', gocui.ModNone, genericTest); err != nil {
|
if err := g.SetKeybinding("stash", gocui.KeySpace, gocui.ModNone, handleStashApply); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// TODO: come up with a better keybinding (p/P used for pushing/pulling which
|
||||||
|
// I'd like to be global. Perhaps all global keybindings should use a modifier
|
||||||
|
// like command? But then there's gonna be hotkey conflicts with the terminal
|
||||||
|
if err := g.SetKeybinding("stash", 'k', gocui.ModNone, handleStashPop); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding("stash", 'd', gocui.ModNone, handleStashDrop); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if err := g.SetKeybinding("", 'S', gocui.ModNone, genericTest); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,9 +192,10 @@ func genericTest(g *gocui.Gui, v *gocui.View) error {
|
|||||||
func layout(g *gocui.Gui) error {
|
func layout(g *gocui.Gui) error {
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
leftSideWidth := width / 3
|
leftSideWidth := width / 3
|
||||||
logsBranchesBoundary := height - 10
|
|
||||||
filesBranchesBoundary := height - 20
|
|
||||||
statusFilesBoundary := 2
|
statusFilesBoundary := 2
|
||||||
|
filesBranchesBoundary := height - 20
|
||||||
|
commitsBranchesBoundary := height - 10
|
||||||
|
commitsStashBoundary := height - 5
|
||||||
|
|
||||||
optionsTop := height - 2
|
optionsTop := height - 2
|
||||||
// hiding options if there's not enough space
|
// hiding options if there's not enough space
|
||||||
@ -191,14 +203,13 @@ func layout(g *gocui.Gui) error {
|
|||||||
optionsTop = height - 1
|
optionsTop = height - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
sideView, err := g.SetView("files", 0, statusFilesBoundary+1, leftSideWidth, filesBranchesBoundary-1)
|
filesView, err := g.SetView("files", 0, statusFilesBoundary+1, leftSideWidth, filesBranchesBoundary-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sideView.Highlight = true
|
filesView.Highlight = true
|
||||||
sideView.Title = "Files"
|
filesView.Title = "Files"
|
||||||
refreshFiles(g)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("status", 0, statusFilesBoundary-2, leftSideWidth, statusFilesBoundary); err != nil {
|
if v, err := g.SetView("status", 0, statusFilesBoundary-2, leftSideWidth, statusFilesBoundary); err != nil {
|
||||||
@ -208,35 +219,36 @@ func layout(g *gocui.Gui) error {
|
|||||||
v.Title = "Status"
|
v.Title = "Status"
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop); err != nil {
|
mainView, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop)
|
||||||
|
if err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Title = "Diff"
|
mainView.Title = "Diff"
|
||||||
v.Wrap = true
|
mainView.Wrap = true
|
||||||
switchFocus(g, nil, v)
|
|
||||||
handleFileSelect(g, sideView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, logsBranchesBoundary-1); err != nil {
|
if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, commitsBranchesBoundary-1); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Title = "Branches"
|
v.Title = "Branches"
|
||||||
|
|
||||||
// these are only called once
|
|
||||||
refreshBranches(g)
|
|
||||||
nextView(g, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("commits", 0, logsBranchesBoundary, leftSideWidth, optionsTop); err != nil {
|
if v, err := g.SetView("commits", 0, commitsBranchesBoundary, leftSideWidth, commitsStashBoundary-1); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Title = "Commits"
|
v.Title = "Commits"
|
||||||
|
|
||||||
// these are only called once
|
}
|
||||||
refreshCommits(g)
|
|
||||||
|
if v, err := g.SetView("stash", 0, commitsStashBoundary, leftSideWidth, optionsTop); err != nil {
|
||||||
|
if err != gocui.ErrUnknownView {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = "Stash"
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2); err != nil {
|
if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2); err != nil {
|
||||||
@ -246,6 +258,14 @@ func layout(g *gocui.Gui) error {
|
|||||||
v.BgColor = gocui.ColorBlue
|
v.BgColor = gocui.ColorBlue
|
||||||
v.Frame = false
|
v.Frame = false
|
||||||
v.Title = "Options"
|
v.Title = "Options"
|
||||||
|
|
||||||
|
// these are only called once
|
||||||
|
handleFileSelect(g, filesView)
|
||||||
|
refreshFiles(g)
|
||||||
|
refreshBranches(g)
|
||||||
|
refreshCommits(g)
|
||||||
|
refreshStashEntries(g)
|
||||||
|
nextView(g, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
83
stash_panel.go
Normal file
83
stash_panel.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func refreshStashEntries(g *gocui.Gui) error {
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
v, err := g.View("stash")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
state.StashEntries = getGitStashEntries()
|
||||||
|
v.Clear()
|
||||||
|
for _, stashEntry := range state.StashEntries {
|
||||||
|
fmt.Fprintln(v, stashEntry.DisplayString)
|
||||||
|
}
|
||||||
|
return resetOrigin(v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSelectedStashEntry(v *gocui.View) *StashEntry {
|
||||||
|
if len(state.StashEntries) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lineNumber := getItemPosition(v)
|
||||||
|
return &state.StashEntries[lineNumber]
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
renderString(g, "options", "space: apply, k: pop, d: drop")
|
||||||
|
go func() {
|
||||||
|
stashEntry := getSelectedStashEntry(v)
|
||||||
|
if stashEntry == nil {
|
||||||
|
renderString(g, "main", "No stash entries")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
diff, _ := getStashEntryDiff(stashEntry.Index)
|
||||||
|
renderString(g, "main", diff)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return stashDo(g, v, "apply")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return stashDo(g, v, "pop")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry? (y/n)", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return stashDo(g, v, "drop")
|
||||||
|
}, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||||
|
stashEntry := getSelectedStashEntry(v)
|
||||||
|
if stashEntry == nil {
|
||||||
|
return createErrorPanel(g, "No stash to "+method)
|
||||||
|
}
|
||||||
|
if output, err := gitStashDo(stashEntry.Index, method); err != nil {
|
||||||
|
createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
refreshStashEntries(g)
|
||||||
|
return refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
|
createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gitStashSave(trimmedContent(v)); err != nil {
|
||||||
|
createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
refreshStashEntries(g)
|
||||||
|
return refreshFiles(g)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
@ -18,7 +18,9 @@ func returnFocus(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
||||||
if oldView != nil {
|
// we assume we'll never want to return focus to a confirmation panel i.e.
|
||||||
|
// we should never stack confirmation panels
|
||||||
|
if oldView != nil && oldView.Name() != "confirmation" {
|
||||||
oldView.Highlight = false
|
oldView.Highlight = false
|
||||||
devLog("setting previous view to:", oldView.Name())
|
devLog("setting previous view to:", oldView.Name())
|
||||||
state.PreviousView = oldView.Name()
|
state.PreviousView = oldView.Name()
|
||||||
@ -38,6 +40,10 @@ func getItemPosition(v *gocui.View) int {
|
|||||||
return oy + cy
|
return oy + cy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trimmedContent(v *gocui.View) string {
|
||||||
|
return strings.TrimSpace(v.Buffer())
|
||||||
|
}
|
||||||
|
|
||||||
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -94,6 +100,7 @@ func correctCursor(v *gocui.View) error {
|
|||||||
func renderString(g *gocui.Gui, viewName, s string) error {
|
func renderString(g *gocui.Gui, viewName, s string) error {
|
||||||
g.Update(func(*gocui.Gui) error {
|
g.Update(func(*gocui.Gui) error {
|
||||||
timeStart := time.Now()
|
timeStart := time.Now()
|
||||||
|
colorLog(color.FgRed, viewName)
|
||||||
v, err := g.View(viewName)
|
v, err := g.View(viewName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user