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:
parent
164ac72c5a
commit
d1ead5b0cf
@ -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
74
gui.go
@ -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
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 {
|
||||
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user