diff --git a/branches_panel.go b/branches_panel.go index bb96947aa..576bc01ec 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -19,13 +19,20 @@ import ( func handleBranchPress(g *gocui.Gui, v *gocui.View) error { branch := getSelectedBranch(v) - if err := gitCheckout(branch.Name, false); err != nil { - panic(err) + if output, err := gitCheckout(branch.Name, false); err != nil { + createSimpleConfirmationPanel(g, v, "Error", output) } - refreshBranches(v) - refreshFiles(g) - refreshLogs(g) - return nil + return refreshSidePanels(g, v) +} + +func handleForceCheckout(g *gocui.Gui, v *gocui.View) error { + branch := getSelectedBranch(v) + return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes (y/n)", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitCheckout(branch.Name, true); err != nil { + createSimpleConfirmationPanel(g, v, "Error", output) + } + return refreshSidePanels(g, v) + }, nil) } func getSelectedBranch(v *gocui.View) Branch { @@ -34,7 +41,7 @@ func getSelectedBranch(v *gocui.View) Branch { } func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { - renderString(g, "options", "space: checkout") + renderString(g, "options", "space: checkout, s: squash down") lineNumber := getItemPosition(v) branch := state.Branches[lineNumber] diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) @@ -44,7 +51,11 @@ func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { return nil } -func refreshBranches(v *gocui.View) error { +func refreshBranches(g *gocui.Gui) error { + v, err := g.View("branches") + if err != nil { + panic(err) + } state.Branches = getGitBranches() yellow := color.New(color.FgYellow) red := color.New(color.FgRed) diff --git a/commit_panel.go b/commit_panel.go index f53f93408..79a09f17b 100644 --- a/commit_panel.go +++ b/commit_panel.go @@ -15,7 +15,7 @@ import ( func handleCommitPress(g *gocui.Gui, currentView *gocui.View) error { devLog(stagedFiles(state.GitFiles)) if len(stagedFiles(state.GitFiles)) == 0 { - return createConfirmationPanel(g, currentView, "Nothing to Commit", "There are no staged files to commit (enter)", nil, nil) + return createSimpleConfirmationPanel(g, currentView, "Nothing to Commit", "There are no staged files to commit (esc)") } maxX, maxY := g.Size() if v, err := g.SetView("commit", maxX/2-30, maxY/2-1, maxX/2+30, maxY/2+1); err != nil { @@ -44,7 +44,7 @@ func handleCommitSubmit(g *gocui.Gui, v *gocui.View) error { panic(err) } refreshFiles(g) - refreshLogs(g) + refreshCommits(g) return closeCommitPrompt(g, v) } diff --git a/commits_panel.go b/commits_panel.go new file mode 100644 index 000000000..2d355a625 --- /dev/null +++ b/commits_panel.go @@ -0,0 +1,79 @@ +// lots of this has been directly ported from one of the example files, will brush up later + +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "github.com/fatih/color" + "github.com/jroimartin/gocui" +) + +func refreshCommits(g *gocui.Gui) error { + state.Commits = getCommits() + g.Update(func(*gocui.Gui) error { + v, err := g.View("commits") + if err != nil { + panic(err) + } + v.Clear() + yellow := color.New(color.FgYellow) + white := color.New(color.FgWhite) + for _, commit := range state.Commits { + yellow.Fprint(v, commit.Sha+" ") + white.Fprintln(v, commit.Name) + } + return nil + }) + return nil +} + +func handleCommitSelect(g *gocui.Gui, v *gocui.View) error { + commit := getSelectedCommit(v) + commitText := gitShow(commit.Sha) + devLog("commitText:", commitText) + return renderString(g, "main", commitText) +} + +func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { + if getItemPosition(v) != 0 { + return createSimpleConfirmationPanel(g, v, "Error", "Can only squash topmost commit") + } + commit := getSelectedCommit(v) + if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil { + return createSimpleConfirmationPanel(g, v, "Error", output) + } + if err := refreshCommits(g); err != nil { + panic(err) + } + return handleCommitSelect(g, v) +} + +func handleRenameCommit(g *gocui.Gui, v *gocui.View) error { + if getItemPosition(v) != 0 { + return createSimpleConfirmationPanel(g, v, "Error", "Can only rename topmost commit") + } + createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitRenameCommit(v.Buffer()); err != nil { + return createSimpleConfirmationPanel(g, v, "Error", output) + } + if err := refreshCommits(g); err != nil { + panic(err) + } + return handleCommitSelect(g, v) + }) + return nil +} + +func getSelectedCommit(v *gocui.View) Commit { + lineNumber := getItemPosition(v) + if len(state.Commits) == 0 { + return Commit{ + Sha: "noCommit", + DisplayString: "none", + } + } + return state.Commits[lineNumber] +} diff --git a/confirmation_panel.go b/confirmation_panel.go index b63534987..08c7e04ab 100644 --- a/confirmation_panel.go +++ b/confirmation_panel.go @@ -11,7 +11,7 @@ import ( // "io" // "io/ioutil" - "math" + "strings" // "strings" "github.com/jroimartin/gocui" @@ -24,39 +24,94 @@ func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) f panic(err) } } - if err := returnFocus(g, v); err != nil { - panic(err) - } - g.DeleteKeybindings("confirmation") - return g.DeleteView("confirmation") + return closeConfirmationPrompt(g) } } +func closeConfirmationPrompt(g *gocui.Gui) error { + view, err := g.View("confirmation") + if err != nil { + panic(err) + } + if err := returnFocus(g, view); err != nil { + panic(err) + } + g.DeleteKeybindings("confirmation") + return g.DeleteView("confirmation") +} + +func getMessageHeight(message string, width int) int { + lines := strings.Split(message, "\n") + lineCount := 0 + for _, line := range lines { + lineCount += len(line)/width + 1 + } + return lineCount +} + func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) { width, height := g.Size() panelWidth := 60 - panelHeight := int(math.Ceil(float64(len(prompt)) / float64(panelWidth))) + // panelHeight := int(math.Ceil(float64(len(prompt)) / float64(panelWidth))) + panelHeight := getMessageHeight(prompt, panelWidth) return width/2 - panelWidth/2, height/2 - panelHeight/2 - panelHeight%2 - 1, width/2 + panelWidth/2, height/2 + panelHeight/2 } -func createConfirmationPanel(g *gocui.Gui, sourceView *gocui.View, title, prompt string, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error { - x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt) - if v, err := g.SetView("confirmation", x0, y0, x1, y1); err != nil { +func createPromptPanel(g *gocui.Gui, v *gocui.View, title string, handleSubmit func(*gocui.Gui, *gocui.View) error) error { + // only need to fit one line + x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "") + if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1); err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = title - renderString(g, "confirmation", prompt+" (y/n)") - switchFocus(g, sourceView, v) - if err := g.SetKeybinding("confirmation", 'n', gocui.ModNone, wrappedConfirmationFunction(handleNo)); err != nil { + confirmationView.Editable = true + g.Cursor = true + confirmationView.Title = title + switchFocus(g, v, confirmationView) + if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleSubmit)); err != nil { return err } - if err := g.SetKeybinding("confirmation", 'y', gocui.ModNone, wrappedConfirmationFunction(handleYes)); err != nil { + if err := g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(nil)); err != nil { return err } } return nil } + +func createConfirmationPanel(g *gocui.Gui, v *gocui.View, title, prompt string, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error { + // delete the existing confirmation panel if it exists + if view, _ := g.View("confirmation"); view != nil { + if err := closeConfirmationPrompt(g); err != nil { + panic(err) + } + } + x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt) + if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + confirmationView.Title = title + renderString(g, "confirmation", prompt) + switchFocus(g, v, confirmationView) + if err := g.SetKeybinding("confirmation", 'n', gocui.ModNone, wrappedConfirmationFunction(handleNo)); err != nil { + return err + } + if err := g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleNo)); err != nil { + return err + } + if err := g.SetKeybinding("confirmation", 'y', gocui.ModNone, wrappedConfirmationFunction(handleYes)); err != nil { + return err + } + if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleYes)); err != nil { + return err + } + } + return nil +} + +func createSimpleConfirmationPanel(g *gocui.Gui, v *gocui.View, title, prompt string) error { + return createConfirmationPanel(g, v, title, prompt, nil, nil) +} diff --git a/files_panel.go b/files_panel.go index 8820ee145..7e55dfbc3 100644 --- a/files_panel.go +++ b/files_panel.go @@ -51,6 +51,7 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error { func getSelectedFile(v *gocui.View) GitFile { lineNumber := getItemPosition(v) if len(state.GitFiles) == 0 { + // find a way to not have to do this return GitFile{ Name: "noFile", DisplayString: "none", @@ -71,7 +72,7 @@ func handleFileRemove(g *gocui.Gui, v *gocui.View) error { } else { deleteVerb = "delete" } - return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error { + return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)? (y/n)", func(g *gocui.Gui, v *gocui.View) error { if err := removeFile(file); err != nil { panic(err) } @@ -135,10 +136,30 @@ func refreshFiles(g *gocui.Gui) error { return nil } -func pullFiles(g *gocui.Gui, v *gocui.Gui) error { - if err := gitPull(); err != nil { - // should show error - panic(err) - } +func pullFiles(g *gocui.Gui, v *gocui.View) error { + devLog("pulling...") + createSimpleConfirmationPanel(g, v, "", "Pulling...") + go func() { + if output, err := gitPull(); err != nil { + createSimpleConfirmationPanel(g, v, "Error", output) + } else { + closeConfirmationPrompt(g) + } + }() + devLog("pulled.") return refreshFiles(g) } + +func pushFiles(g *gocui.Gui, v *gocui.View) error { + devLog("pushing...") + createSimpleConfirmationPanel(g, v, "", "Pushing...") + go func() { + if output, err := gitPush(); err != nil { + createSimpleConfirmationPanel(g, v, "Error", output) + } else { + closeConfirmationPrompt(g) + } + }() + devLog("pushed.") + return nil +} diff --git a/gitcommands.go b/gitcommands.go index b4fca3835..b78da318b 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -11,6 +11,8 @@ import ( "os" "os/exec" "strings" + + "github.com/fatih/color" ) // GitFile : A staged/unstaged file @@ -31,20 +33,32 @@ type Branch struct { BaseBranch string } +// Commit : A git commit +type Commit struct { + Sha string + Name string + DisplayString string +} + func devLog(objects ...interface{}) { - localLog("/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...) +} + +func colorLog(colour color.Attribute, objects ...interface{}) { + localLog(colour, "/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", objects...) } func commandLog(objects ...interface{}) { - localLog("/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/commands.log", objects...) - localLog("/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", objects...) + localLog(color.FgWhite, "/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/commands.log", objects...) + localLog(color.FgWhite, "/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", objects...) } -func localLog(path string, objects ...interface{}) { +func localLog(colour color.Attribute, path string, objects ...interface{}) { f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) defer f.Close() for _, object := range objects { - f.WriteString(fmt.Sprint(object) + "\n") + colorFunction := color.New(colour).SprintFunc() + f.WriteString(colorFunction(fmt.Sprint(object)) + "\n") } } @@ -85,7 +99,7 @@ func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { func runDirectCommand(command string) (string, error) { commandLog(command) - cmdOut, err := exec.Command("bash", "-c", command).Output() + cmdOut, err := exec.Command("bash", "-c", command).CombinedOutput() devLog(string(cmdOut)) devLog(err) return string(cmdOut), err @@ -120,7 +134,7 @@ func getGitBranches() []Branch { baseBranch = name } if i == 0 { - line = line[:2] + "\t*" + line[2:] + line = "* " + line } branches = append(branches, Branch{name, line, branchType, baseBranch}) } @@ -156,19 +170,18 @@ func getGitStatusFiles() []GitFile { return gitFiles } -func gitCheckout(branch string, force bool) error { +func gitCheckout(branch string, force bool) (string, error) { forceArg := "" if force { forceArg = "--force " } - _, err := runCommand("git checkout " + forceArg + branch) - return err + return runCommand("git checkout " + forceArg + branch) } -func runCommand(cmd string) (string, error) { - commandLog(cmd) - splitCmd := strings.Split(cmd, " ") - cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).Output() +func runCommand(command string) (string, error) { + commandLog(command) + splitCmd := strings.Split(command, " ") + cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() devLog(string(cmdOut[:])) return string(cmdOut), err } @@ -185,8 +198,30 @@ func getBranchDiff(branch string, baseBranch string) (string, error) { return runCommand("git diff --color " + baseBranch + "..." + branch) } +func getCommits() []Commit { + log := getLog() + commits := make([]Commit, 0) + // now we can split it up and turn it into commits + lines := splitLines(log) + for _, line := range lines { + splitLine := strings.Split(line, " ") + commits = append(commits, Commit{splitLine[0], strings.Join(splitLine[1:], " "), strings.Join(splitLine, " ")}) + } + devLog(commits) + return commits +} + func getLog() string { - result, err := runDirectCommand("git log --color --oneline") + result, err := runDirectCommand("git log --oneline") + if err != nil { + panic(err) + } + return result +} + +func gitShow(sha string) string { + result, err := runDirectCommand("git show --color " + sha) + // result, err := runDirectCommand("git show --color 10fd353") if err != nil { panic(err) } @@ -245,9 +280,34 @@ func gitCommit(message string) error { return err } -func gitPull() error { - _, err := runDirectCommand("git pull --no-edit") - return err +func gitPull() (string, error) { + return runDirectCommand("git pull --no-edit") +} + +func gitPush() (string, error) { + return runDirectCommand("git push -u") +} + +func gitSquashPreviousTwoCommits(message string) (string, error) { + return runDirectCommand("git reset --soft head^ && git commit --amend -m \"" + message + "\"") +} + +func gitRenameCommit(message string) (string, error) { + return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"") +} + +func betterHaveWorked(err error) { + if err != nil { + panic(err) + } +} + +func gitUpstreamDifferenceCount() (string, string) { + pushableCount, err := runDirectCommand("git rev-list @{u}..head --count") + betterHaveWorked(err) + pullableCount, err := runDirectCommand("git rev-list head..@{u} --count") + betterHaveWorked(err) + return pullableCount, pushableCount } const getBranchesCommand = `set -e @@ -263,7 +323,7 @@ git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD | { printf "%s\t%s\n" "$date" "$branch" fi fi - done | sed 's/ days /d /g' | sed 's/ weeks /w /g' | sed 's/ hours /h /g' | sed 's/ minutes /m /g' | sed 's/ago//g' | tr -d ' ' + done | sed 's/ days /d /g' | sed 's/ weeks /w /g' | sed 's/ hours /h /g' | sed 's/ minutes /m /g' | sed 's/ seconds /m /g' | sed 's/ago//g' | tr -d ' ' } ` diff --git a/gui.go b/gui.go index 08bbe8fbe..442433981 100644 --- a/gui.go +++ b/gui.go @@ -20,15 +20,24 @@ import ( type stateType struct { GitFiles []GitFile Branches []Branch + Commits []Commit PreviousView string } var state = stateType{ GitFiles: make([]GitFile, 0), PreviousView: "files", + Commits: make([]Commit, 0), } -var cyclableViews = []string{"files", "branches"} +var cyclableViews = []string{"files", "branches", "commits"} + +func refreshSidePanels(g *gocui.Gui, v *gocui.View) error { + refreshBranches(g) + refreshFiles(g) + refreshCommits(g) + return nil +} func nextView(g *gocui.Gui, v *gocui.View) error { var focusedViewName string @@ -41,7 +50,8 @@ func nextView(g *gocui.Gui, v *gocui.View) error { break } if i == len(cyclableViews)-1 { - panic(v.Name() + " is not in the list of views") + devLog(v.Name() + " is not in the list of views") + return nil } } } @@ -68,6 +78,8 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error { return nil case "main": return nil + case "commits": + return handleCommitSelect(g, v) default: panic("No view matching newLineFocused switch statement") } @@ -113,19 +125,25 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil { return err } - if err := g.SetKeybinding("", 'ç', gocui.ModNone, handleCommitPress); err != nil { + if err := g.SetKeybinding("files", 'c', gocui.ModNone, handleCommitPress); err != nil { return err } if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil { return err } - if err := g.SetKeybinding("files", '®', gocui.ModNone, handleFileRemove); err != nil { + if err := g.SetKeybinding("files", 'r', gocui.ModNone, handleFileRemove); err != nil { return err } - if err := g.SetKeybinding("files", 'ø', gocui.ModNone, handleFileOpen); err != nil { + if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil { return err } - if err := g.SetKeybinding("files", 'ß', gocui.ModNone, handleSublimeFileOpen); err != nil { + if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { + return err + } + if err := g.SetKeybinding("files", 'p', gocui.ModNone, pullFiles); err != nil { + return err + } + if err := g.SetKeybinding("files", 'P', gocui.ModNone, pushFiles); err != nil { return err } if err := g.SetKeybinding("commit", gocui.KeyEsc, gocui.ModNone, closeCommitPrompt); err != nil { @@ -137,6 +155,15 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { return err } + if err := g.SetKeybinding("branches", 'F', gocui.ModNone, handleForceCheckout); err != nil { + return err + } + if err := g.SetKeybinding("commits", 's', gocui.ModNone, handleCommitSquashDown); err != nil { + return err + } + if err := g.SetKeybinding("commits", 'r', gocui.ModNone, handleRenameCommit); err != nil { + return err + } if err := g.SetKeybinding("", '∑', gocui.ModNone, handleLogState); err != nil { return err } @@ -146,6 +173,7 @@ func keybindings(g *gocui.Gui) error { func handleLogState(g *gocui.Gui, v *gocui.View) error { devLog("state is:", state) devLog("previous view:", state.PreviousView) + refreshBranches(g) return nil } @@ -171,7 +199,7 @@ func layout(g *gocui.Gui) error { refreshFiles(g) } - if v, err := g.SetView("main", leftSideWidth+2, 0, width-1, optionsTop-1); err != nil { + if v, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop-1); err != nil { if err != gocui.ErrUnknownView { return err } @@ -181,14 +209,14 @@ func layout(g *gocui.Gui) error { handleFileSelect(g, sideView) } - if v, err := g.SetView("logs", 0, logsBranchesBoundary, leftSideWidth, optionsTop-1); err != nil { + if v, err := g.SetView("commits", 0, logsBranchesBoundary, leftSideWidth, optionsTop-1); err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = "Log" + v.Title = "Commits" // these are only called once - refreshLogs(g) + refreshCommits(g) } if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, logsBranchesBoundary-1); err != nil { @@ -198,7 +226,7 @@ func layout(g *gocui.Gui) error { v.Title = "Branches" // these are only called once - refreshBranches(v) + refreshBranches(g) nextView(g, nil) } diff --git a/logs_panel.go b/logs_panel.go deleted file mode 100644 index aa6f1ce48..000000000 --- a/logs_panel.go +++ /dev/null @@ -1,30 +0,0 @@ -// lots of this has been directly ported from one of the example files, will brush up later - -// Copyright 2014 The gocui Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import ( - "fmt" - - "github.com/jroimartin/gocui" -) - -func refreshLogs(g *gocui.Gui) error { - // here is where you want to pickup from - // state.Logs = getGitLogs(nil) - s := getLog() - g.Update(func(*gocui.Gui) error { - v, err := g.View("logs") - v.Clear() - if err != nil { - panic(err) - } - v.Clear() - fmt.Fprint(v, s) - return nil - }) - return nil -} diff --git a/main.go b/main.go index 15848b980..36d35ff68 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,10 @@ package main +import "github.com/fatih/color" + func main() { + a, b := gitUpstreamDifferenceCount() + colorLog(color.FgRed, a, b) devLog("\n\n\n\n\n\n\n\n\n\n") run() } diff --git a/view_helpers.go b/view_helpers.go index fe643c5a7..b8acb524b 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -32,7 +32,7 @@ func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { if _, err := g.SetCurrentView(newView.Name()); err != nil { return err } - g.Cursor = newView.Name() == "commit" + g.Cursor = newView.Editable return newLineFocused(g, newView) }