1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-19 21:28:28 +02:00

add stash panel

This commit is contained in:
Jesse Duffield 2018-06-05 18:48:46 +10:00
parent 164ac72c5a
commit d1ead5b0cf
4 changed files with 192 additions and 31 deletions

View File

@ -3,6 +3,7 @@ package main
import (
// "log"
"errors"
"fmt"
"os"
"os/exec"
@ -13,6 +14,7 @@ import (
)
// GitFile : A staged/unstaged file
// TODO: decide whether to give all of these the Git prefix
type GitFile struct {
Name string
HasStagedChanges bool
@ -39,6 +41,13 @@ type Commit struct {
DisplayString string
}
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
func devLog(objects ...interface{}) {
localLog(color.FgWhite, "/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", objects...)
}
@ -178,13 +187,34 @@ func getGitBranches() []Branch {
return branches
}
rawString, _ := runDirectCommand(getBranchesCommand)
branchLines := splitLines(rawString)
for i, line := range branchLines {
for i, line := range splitLines(rawString) {
branches = append(branches, branchFromLine(line, i))
}
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 {
statusOutput, _ := getGitStatus()
statusStrings := splitLines(statusOutput)
@ -208,6 +238,23 @@ func getGitStatusFiles() []GitFile {
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) {
forceArg := ""
if force {
@ -369,6 +416,10 @@ func gitNewBranch(name string) (string, error) {
return runDirectCommand("git checkout -b " + name)
}
func gitListStash() (string, error) {
return runDirectCommand("git stash list")
}
func gitUpstreamDifferenceCount() (string, string) {
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
if err != nil {
@ -378,7 +429,7 @@ func gitUpstreamDifferenceCount() (string, string) {
if err != nil {
return "?", "?"
}
return strings.Trim(pushableCount, " \n"), strings.Trim(pullableCount, " \n")
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
func gitCommitsToPush() []string {

74
gui.go
View File

@ -15,6 +15,7 @@ type stateType struct {
GitFiles []GitFile
Branches []Branch
Commits []Commit
StashEntries []StashEntry
PreviousView string
}
@ -22,9 +23,10 @@ var state = stateType{
GitFiles: make([]GitFile, 0),
PreviousView: "files",
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 {
refreshBranches(g)
@ -66,14 +68,14 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error {
return handleFileSelect(g, v)
case "branches":
return handleBranchSelect(g, v)
case "commit":
return handleCommitPromptFocus(g, v)
case "confirmation":
return nil
case "main":
return nil
case "commits":
return handleCommitSelect(g, v)
case "stash":
return handleStashEntrySelect(g, v)
default:
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 {
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
}
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 {
return err
}
if err := g.SetKeybinding("commit", gocui.KeyEsc, gocui.ModNone, closeCommitPrompt); err != nil {
return err
}
if err := g.SetKeybinding("commit", gocui.KeyEnter, gocui.ModNone, handleCommitSubmit); err != nil {
if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil {
return err
}
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 {
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
}
// 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
}
@ -181,9 +192,10 @@ func genericTest(g *gocui.Gui, v *gocui.View) error {
func layout(g *gocui.Gui) error {
width, height := g.Size()
leftSideWidth := width / 3
logsBranchesBoundary := height - 10
filesBranchesBoundary := height - 20
statusFilesBoundary := 2
filesBranchesBoundary := height - 20
commitsBranchesBoundary := height - 10
commitsStashBoundary := height - 5
optionsTop := height - 2
// hiding options if there's not enough space
@ -191,14 +203,13 @@ func layout(g *gocui.Gui) error {
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 != gocui.ErrUnknownView {
return err
}
sideView.Highlight = true
sideView.Title = "Files"
refreshFiles(g)
filesView.Highlight = true
filesView.Title = "Files"
}
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"
}
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 {
return err
}
v.Title = "Diff"
v.Wrap = true
switchFocus(g, nil, v)
handleFileSelect(g, sideView)
mainView.Title = "Diff"
mainView.Wrap = true
}
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 {
return err
}
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 {
return err
}
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 {
@ -246,6 +258,14 @@ func layout(g *gocui.Gui) error {
v.BgColor = gocui.ColorBlue
v.Frame = false
v.Title = "Options"
// these are only called once
handleFileSelect(g, filesView)
refreshFiles(g)
refreshBranches(g)
refreshCommits(g)
refreshStashEntries(g)
nextView(g, nil)
}
return nil

83
stash_panel.go Normal file
View 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
}

View File

@ -18,7 +18,9 @@ func returnFocus(g *gocui.Gui, v *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
devLog("setting previous view to:", oldView.Name())
state.PreviousView = oldView.Name()
@ -38,6 +40,10 @@ func getItemPosition(v *gocui.View) int {
return oy + cy
}
func trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
if v == nil {
return nil
@ -94,6 +100,7 @@ func correctCursor(v *gocui.View) error {
func renderString(g *gocui.Gui, viewName, s string) error {
g.Update(func(*gocui.Gui) error {
timeStart := time.Now()
colorLog(color.FgRed, viewName)
v, err := g.View(viewName)
if err != nil {
panic(err)