From cd5a2a3c6d39d6d828c1b6c95d47b50eb4fabcc3 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 20 May 2018 19:03:24 +1000 Subject: [PATCH 01/32] test file --- testfile | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 testfile diff --git a/testfile b/testfile new file mode 100644 index 000000000..e69de29bb From 6c817669725b73cdbfc24059a8ea39a59cbc86b0 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 20 May 2018 19:05:21 +1000 Subject: [PATCH 02/32] test file on hotfix branch --- anothertestfile | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 anothertestfile diff --git a/anothertestfile b/anothertestfile new file mode 100644 index 000000000..e69de29bb From 30c4c6e576a39915d7a4d9406015ade32660f4cc Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 20 May 2018 19:06:04 +1000 Subject: [PATCH 03/32] another test file adding stuff --- anothertestfile | 1 + 1 file changed, 1 insertion(+) diff --git a/anothertestfile b/anothertestfile index e69de29bb..d6b7cdfc5 100644 --- a/anothertestfile +++ b/anothertestfile @@ -0,0 +1 @@ +test stuff From ac1fa346acd7ba24eace2953359f16e47515645b Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 20:52:48 +1000 Subject: [PATCH 04/32] test file --- gitcommands.go | 211 ++++++++++++++++++++-------- gui.go | 375 +++++++++++++++++++++++++++++++++++++------------ testFile.txt | 1 + 3 files changed, 438 insertions(+), 149 deletions(-) create mode 100644 testFile.txt diff --git a/gitcommands.go b/gitcommands.go index 94e71686d..465a61da1 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -5,92 +5,164 @@ package main import ( - "fmt" + // "log" + "fmt" "os/exec" - "os" "strings" - "regexp" - "runtime" ) +// GitFile : A staged/unstaged file +type GitFile struct { + Name string + DisplayString string + HasStagedChanges bool + HasUnstagedChanges bool + Tracked bool + Deleted bool +} + +// Branch : A git branch +type Branch struct { + Name string + DisplayString string + Type string + BaseBranch string +} + // Map (from https://gobyexample.com/collection-functions) func Map(vs []string, f func(string) string) []string { - vsm := make([]string, len(vs)) - for i, v := range vs { - vsm[i] = f(v) - } - return vsm + vsm := make([]string, len(vs)) + for i, v := range vs { + vsm[i] = f(v) + } + return vsm } -func sanitisedFileString(fileString string) string { - r := regexp.MustCompile("\\s| \\(new commits\\)|.* ") - fileString = r.ReplaceAllString(fileString, "") - return fileString -} - -func filesByMatches(statusString string, targets []string) []string { - files := make([]string, 0) - for _, target := range targets { - if strings.Index(statusString, target) == -1 { - continue - } - r := regexp.MustCompile("(?s)" + target + ".*?\n\n(.*?)\n\n") - // fmt.Println(r) - - matchedFileStrings := strings.Split(r.FindStringSubmatch(statusString)[1], "\n") - // fmt.Println(matchedFileStrings) - - matchedFiles := Map(matchedFileStrings, sanitisedFileString) - // fmt.Println(matchedFiles) - files = append(files, matchedFiles...) - +func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { + if len(oldGitFiles) == 0 { + return newGitFiles } - breakHere() - - // fmt.Println(files) - return files -} - -func breakHere() { - if len(os.Args) > 1 && os.Args[1] == "debug" { - runtime.Breakpoint() + result := make([]GitFile, 0) + for _, oldGitFile := range oldGitFiles { + for _, newGitFile := range newGitFiles { + if oldGitFile.Name == newGitFile.Name { + result = append(result, newGitFile) + break + } + } } + return result } -func getFilesToStage(statusString string) []string { - targets := []string{"Changes not staged for commit:", "Untracked files:"} - return filesByMatches(statusString, targets) +func getGitBranchOutput() (string, error) { + cmdOut, err := exec.Command("bash", "-c", getBranchesCommand).Output() + return string(cmdOut), err } -func getFilesToUnstage(statusString string) []string { - targets := []string{"Changes to be committed:"} - return filesByMatches(statusString, targets) +func branchNameFromString(branchString string) string { + // because this has the recency at the beginning, + // we need to split and take the second part + splitBranchName := strings.Split(branchString, "\t") + return splitBranchName[len(splitBranchName)-1] +} + +func getGitBranches() []Branch { + branches := make([]Branch, 0) + rawString, _ := getGitBranchOutput() + branchLines := splitLines(rawString) + for _, line := range branchLines { + name := branchNameFromString(line) + var branchType string + var baseBranch string + if strings.Contains(line, "feature/") { + branchType = "feature" + baseBranch = "develop" + } else if strings.Contains(line, "bugfix/") { + branchType = "bugfix" + baseBranch = "develop" + } else if strings.Contains(line, "hotfix/") { + branchType = "hotfix" + baseBranch = "master" + } else { + branchType = "other" + baseBranch = name + } + branches = append(branches, Branch{name, line, branchType, baseBranch}) + } + devLog(fmt.Sprint(branches)) + return branches +} + +func getGitStatusFiles() []GitFile { + statusOutput, _ := getGitStatus() + statusStrings := splitLines(statusOutput) + devLog(fmt.Sprint(statusStrings)) + // a file can have both staged and unstaged changes + // I'll probably end up ignoring the unstaged flag for now but might revisit + // tracked, staged, unstaged + + gitFiles := make([]GitFile, 0) + + for _, statusString := range statusStrings { + stagedChange := statusString[0:1] + unstagedChange := statusString[1:2] + filename := statusString[3:] + tracked := statusString[0:2] != "??" + gitFile := GitFile{ + Name: filename, + DisplayString: statusString, + HasStagedChanges: tracked && stagedChange != " ", + HasUnstagedChanges: !tracked || unstagedChange != " ", + Tracked: tracked, + Deleted: unstagedChange == "D" || stagedChange == "D", + } + gitFiles = append(gitFiles, gitFile) + } + return gitFiles +} + +func gitCheckout(branch string, force bool) error { + forceArg := "" + if force { + forceArg = "--force " + } + _, err := runCommand("git checkout " + forceArg + branch) + return err } func runCommand(cmd string) (string, error) { splitCmd := strings.Split(cmd, " ") cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).Output() + devLog(cmd) + devLog(string(cmdOut)) return string(cmdOut), err } -func getDiff(file string, cached bool) string { - devLog(file) +func getBranchDiff(branch string, baseBranch string) (string, error) { + return runCommand("git diff --color " + baseBranch + "..." + branch) +} + +func getDiff(file GitFile) string { cachedArg := "" - if cached { + if file.HasStagedChanges { cachedArg = "--cached " } - s, err := runCommand("git diff " + cachedArg + file) + deletedArg := "" + if file.Deleted || !file.Tracked { + deletedArg = "--no-index /dev/null " + } + command := "git diff --color " + cachedArg + deletedArg + file.Name + s, err := runCommand(command) if err != nil { // for now we assume an error means the file was deleted - return "deleted" + return s } return s } func stageFile(file string) error { - devLog("staging " + file) _, err := runCommand("git add " + file) return err } @@ -100,13 +172,34 @@ func unStageFile(file string) error { return err } -func testGettingFiles() { - - statusString, _ := runCommand("git status") - fmt.Println(getFilesToStage(statusString)) - fmt.Println(getFilesToUnstage(statusString)) - - runCommand("git add hello-world.go") +func getGitStatus() (string, error) { + return runCommand("git status --untracked-files=all --short") } +const getBranchesCommand = `set -e +git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD | { + seen=":" + git_dir="$(git rev-parse --git-dir)" + while read line; do + date="${line%%|*}" + branch="${line##* }" + if ! [[ $seen == *:"${branch}":* ]]; then + seen="${seen}${branch}:" + if [ -f "${git_dir}/refs/heads/${branch}" ]; then + 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 ' ' +} +` +// func main() { +// getGitStatusFiles() +// } + +// func devLog(s string) { +// f, _ := os.OpenFile("development.log", os.O_APPEND|os.O_WRONLY, 0644) +// defer f.Close() + +// f.WriteString(s + "\n") +// } diff --git a/gui.go b/gui.go index f58d13fdb..63566c737 100644 --- a/gui.go +++ b/gui.go @@ -1,3 +1,5 @@ +// 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. @@ -6,95 +8,213 @@ package main import ( "fmt" + "strings" // "io" // "io/ioutil" "log" // "strings" "os" - "github.com/jroimartin/gocui" + "github.com/fatih/color" + "github.com/jroimartin/gocui" ) -type gitFile struct { - Name string - Staged bool +type stateType struct { + GitFiles []GitFile + Branches []Branch } -var gitFiles []gitFile +var state = stateType{GitFiles: make([]GitFile, 0)} + +var cyclableViews = []string{"files", "branches"} func nextView(g *gocui.Gui, v *gocui.View) error { - if v == nil || v.Name() == "side" { - _, err := g.SetCurrentView("main") + var focusedViewName string + if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { + focusedViewName = cyclableViews[0] + } else { + for i := range cyclableViews { + if v.Name() == cyclableViews[i] { + focusedViewName = cyclableViews[i+1] + break + } + if i == len(cyclableViews)-1 { + panic(v.Name() + " is not in the list of views") + } + } + } + focusedView, err := g.View(focusedViewName) + if err != nil { + panic(err) return err } - _, err := g.SetCurrentView("side") + if v != nil { + v.Highlight = false + } + focusedView.Highlight = true + devLog(focusedViewName) + _, err = g.SetCurrentView(focusedViewName) + itemSelected(g, focusedView) + showViewOptions(g, focusedViewName) return err } +func showViewOptions(g *gocui.Gui, viewName string) error { + optionsMap := map[string]string{ + "files": "space: toggle staged, c: commit changes", + "branches": "space: checkout", + } + g.Update(func(*gocui.Gui) error { + v, err := g.View("options") + if err != nil { + panic(err) + } + v.Clear() + fmt.Fprint(v, optionsMap[viewName]) + return nil + }) + return nil +} + +func getItemPosition(v *gocui.View) int { + _, cy := v.Cursor() + _, oy := v.Origin() + return oy + cy +} + +func cursorUp(g *gocui.Gui, v *gocui.View) error { + if v == nil { + return nil + } + + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + + itemSelected(g, v) + return nil +} + +func resetOrigin(v *gocui.View) error { + if err := v.SetCursor(0, 0); err != nil { + return err + } + return v.SetOrigin(0, 0) +} + func cursorDown(g *gocui.Gui, v *gocui.View) error { if v != nil { cx, cy := v.Cursor() + ox, oy := v.Origin() + if cy+oy >= len(v.BufferLines())-2 { + return nil + } if err := v.SetCursor(cx, cy+1); err != nil { - ox, oy := v.Origin() if err := v.SetOrigin(ox, oy+1); err != nil { return err } } } - // refresh main panel's text to match newly selected item - return handleItemSelect(g, v) + itemSelected(g, v) + return nil } -func cursorUp(g *gocui.Gui, v *gocui.View) error { - if v != nil { - ox, oy := v.Origin() - cx, cy := v.Cursor() - if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { - if err := v.SetOrigin(ox, oy-1); err != nil { - return err - } - } - } +func itemSelected(g *gocui.Gui, v *gocui.View) error { + mainView, _ := g.View("main") + mainView.SetOrigin(0, 0) - // refresh main panel's text to match newly selected item - return handleItemSelect(g, v) + switch v.Name() { + case "files": + return handleFileSelect(g, v) + case "branches": + return handleBranchSelect(g, v) + default: + panic("No view matching itemSelected switch statement") + } +} + +func scrollUp(g *gocui.Gui, v *gocui.View) error { + mainView, _ := g.View("main") + ox, oy := mainView.Origin() + if oy >= 1 { + return mainView.SetOrigin(ox, oy-1) + } + return nil +} + +func scrollDown(g *gocui.Gui, v *gocui.View) error { + mainView, _ := g.View("main") + ox, oy := mainView.Origin() + if oy < len(mainView.BufferLines()) { + return mainView.SetOrigin(ox, oy+1) + } + return nil } func devLog(s string) { - f, _ := os.OpenFile("development.log", os.O_APPEND|os.O_WRONLY, 0644) + f, _ := os.OpenFile("/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", os.O_APPEND|os.O_WRONLY, 0644) defer f.Close() f.WriteString(s + "\n") } -func handleItemPress(g *gocui.Gui, v *gocui.View) error { - item := getItem(v) +func handleBranchPress(g *gocui.Gui, v *gocui.View) error { + branch := getSelectedBranch(v) + if err := gitCheckout(branch.Name, false); err != nil { + return err + } + refreshBranches(v) + refreshFiles(g) + return nil +} - if item.Staged { - unStageFile(item.Name) +func handleFilePress(g *gocui.Gui, v *gocui.View) error { + file := getSelectedFile(v) + + if file.HasUnstagedChanges { + stageFile(file.Name) } else { - stageFile(item.Name) + unStageFile(file.Name) } - if err := refreshList(v); err != nil { + if err := refreshFiles(g); err != nil { + return err + } + if err := handleFileSelect(g, v); err != nil { + return err + } + + return nil +} + +func getSelectedFile(v *gocui.View) GitFile { + lineNumber := getItemPosition(v) + return state.GitFiles[lineNumber] +} + +func getSelectedBranch(v *gocui.View) Branch { + lineNumber := getItemPosition(v) + return state.Branches[lineNumber] +} + +func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { + lineNumber := getItemPosition(v) + branch := state.Branches[lineNumber] + diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) + if err := renderString(g, diff); err != nil { return err } return nil } -func getItem(v *gocui.View) gitFile { - _, lineNumber := v.Cursor() - if lineNumber >= len(gitFiles) { - return gitFiles[len(gitFiles) - 1] - } - return gitFiles[lineNumber] -} - -func handleItemSelect(g *gocui.Gui, v *gocui.View) error { - item := getItem(v) - diff := getDiff(item.Name, item.Staged) - devLog(diff) +func handleFileSelect(g *gocui.Gui, v *gocui.View) error { + item := getSelectedFile(v) + diff := getDiff(item) if err := renderString(g, diff); err != nil { return err } @@ -102,7 +222,7 @@ func handleItemSelect(g *gocui.Gui, v *gocui.View) error { // maxX, maxY := g.Size() // if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil { // if err != gocui.ErrUnknownView { - // return err + // return errkjhgkhj // } // fmt.Fprintln(v, l) // if _, err := g.SetCurrentView("msg"); err != nil { @@ -116,7 +236,7 @@ func delMsg(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView("msg"); err != nil { return err } - if _, err := g.SetCurrentView("side"); err != nil { + if _, err := g.SetCurrentView("files"); err != nil { return err } return nil @@ -127,79 +247,154 @@ func quit(g *gocui.Gui, v *gocui.View) error { } func keybindings(g *gocui.Gui) error { - if err := g.SetKeybinding("side", gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + for _, view := range cyclableViews { + if err := g.SetKeybinding(view, gocui.KeyTab, gocui.ModNone, nextView); err != nil { + return err + } + if err := g.SetKeybinding(view, 'q', gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding(view, gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding(view, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + return err + } + if err := g.SetKeybinding(view, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + return err + } + if err := g.SetKeybinding(view, gocui.KeyPgup, gocui.ModNone, scrollUp); err != nil { + return err + } + if err := g.SetKeybinding(view, gocui.KeyPgdn, gocui.ModNone, scrollDown); err != nil { + return err + } + } + if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil { return err } - if err := g.SetKeybinding("main", gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { return err } - if err := g.SetKeybinding("side", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { - return err - } - if err := g.SetKeybinding("side", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding("side", gocui.KeySpace, gocui.ModNone, handleItemPress); err != nil { - return err - } - // if err := g.SetKeybinding("msg", gocui.KeySpace, gocui.ModNone, delMsg); err != nil { - // return err - // } return nil } -func refreshList(v *gocui.View) error { - // get files to stage - statusString, _ := runCommand("git status") - filesToStage := getFilesToStage(statusString) - filesToUnstage := getFilesToUnstage(statusString) - // v.Highlight = true - // v.SelBgColor = gocui.ColorWhite - // v.SelFgColor = gocui.ColorBlack - v.Clear() - gitFiles = make([]gitFile, 0) +func splitLines(multilineString string) []string { + if multilineString == "" || multilineString == "\n" { + return make([]string, 0) + } + lines := strings.Split(multilineString, "\n") + if lines[len(lines)-1] == "" { + return lines[:len(lines)-1] + } + return lines +} + +func refreshBranches(v *gocui.View) error { + state.Branches = getGitBranches() + yellow := color.New(color.FgYellow) red := color.New(color.FgRed) - for _, file := range filesToStage { - gitFiles = append(gitFiles, gitFile{file, false}) - red.Fprintln(v, file) - } + white := color.New(color.FgWhite) green := color.New(color.FgGreen) - for _, file := range filesToUnstage { - gitFiles = append(gitFiles, gitFile{file, true}) - green.Fprintln(v, file) + + v.Clear() + for _, branch := range state.Branches { + if branch.Type == "feature" { + green.Fprintln(v, branch.DisplayString) + continue + } + if branch.Type == "bugfix" { + yellow.Fprintln(v, branch.DisplayString) + continue + } + if branch.Type == "hotfix" { + red.Fprintln(v, branch.DisplayString) + continue + } + white.Fprintln(v, branch.DisplayString) + } + resetOrigin(v) + return nil +} + +func refreshFiles(g *gocui.Gui) error { + filesView, err := g.View("files") + if err != nil { + return err + } + + // get files to stage + gitFiles := getGitStatusFiles() + state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) + + filesView.Clear() + red := color.New(color.FgRed) + green := color.New(color.FgGreen) + for _, gitFile := range state.GitFiles { + if !gitFile.Tracked { + red.Fprintln(filesView, gitFile.DisplayString) + continue + } + green.Fprint(filesView, gitFile.DisplayString[0:1]) + red.Fprint(filesView, gitFile.DisplayString[1:3]) + if gitFile.HasUnstagedChanges { + red.Fprintln(filesView, gitFile.Name) + } else { + green.Fprintln(filesView, gitFile.Name) + } } - devLog(fmt.Sprint(gitFiles)) return nil } func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - sideView, err := g.SetView("side", -1, -1, 30, maxY) + leftSideWidth := maxX / 3 + filesBranchesBoundary := maxY - 10 + + optionsTop := maxY - 3 + // hiding options if there's not enough space + if maxY < 30 { + optionsTop = maxY + } + + sideView, err := g.SetView("files", 0, 0, leftSideWidth, filesBranchesBoundary-1) if err != nil { if err != gocui.ErrUnknownView { return err } + sideView.Highlight = true sideView.Title = "Files" - devLog("test") - refreshList(sideView) + refreshFiles(g) } - if v, err := g.SetView("main", 30, -1, maxX, maxY); err != nil { + if v, err := g.SetView("main", leftSideWidth+2, 0, maxX-1, optionsTop-1); err != nil { if err != gocui.ErrUnknownView { return err } - v.Editable = true + v.Title = "Diff" v.Wrap = true - if _, err := g.SetCurrentView("side"); err != nil { + if _, err := g.SetCurrentView("files"); err != nil { return err } - handleItemSelect(g, sideView) + handleFileSelect(g, sideView) + } + + if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, optionsTop-1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Branches" + + // these are only called once + refreshBranches(v) + nextView(g, nil) + } + + if v, err := g.SetView("options", 0, optionsTop, maxX-1, optionsTop+2); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Options" } return nil @@ -226,7 +421,7 @@ func run() { } defer g.Close() - g.Cursor = true + // g.Cursor = true g.SetManagerFunc(layout) diff --git a/testFile.txt b/testFile.txt new file mode 100644 index 000000000..16b14f5da --- /dev/null +++ b/testFile.txt @@ -0,0 +1 @@ +test file From 9fc194f0cdd6b3f404f8a1929bafb8c90fa28c9d Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:08:58 +1000 Subject: [PATCH 05/32] Test commit --- testFile.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/testFile.txt b/testFile.txt index 16b14f5da..4c88aab13 100644 --- a/testFile.txt +++ b/testFile.txt @@ -1 +1,2 @@ test file +hmm From 810bd54e680db3776eb099944d7dcf99802dcd1c Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:10:43 +1000 Subject: [PATCH 06/32] "test" --- test | 1 + 1 file changed, 1 insertion(+) create mode 100644 test diff --git a/test b/test new file mode 100644 index 000000000..c1a7bd5f1 --- /dev/null +++ b/test @@ -0,0 +1 @@ +blah blah blah From c7f4af2b4155010b0d070d3a41032abfa5a3f86e Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:14:16 +1000 Subject: [PATCH 07/32] "Test" --- testFile2.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 testFile2.txt diff --git a/testFile2.txt b/testFile2.txt new file mode 100644 index 000000000..1cf5b232c --- /dev/null +++ b/testFile2.txt @@ -0,0 +1 @@ +hmm From 3cb228aac871afb3fa41af79b58ed430a71de7fa Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:19:00 +1000 Subject: [PATCH 08/32] test message --- testFile2.txt | 1 + testFile3.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 testFile3.txt diff --git a/testFile2.txt b/testFile2.txt index 1cf5b232c..3d8f5ddc6 100644 --- a/testFile2.txt +++ b/testFile2.txt @@ -1 +1,2 @@ hmm +hmm diff --git a/testFile3.txt b/testFile3.txt new file mode 100644 index 000000000..1cf5b232c --- /dev/null +++ b/testFile3.txt @@ -0,0 +1 @@ +hmm From dfbbad5d958a8cd23c135c6c2adbea6b8971ec07 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:20:51 +1000 Subject: [PATCH 09/32] Test again --- testFile.txt | 2 ++ testFile3.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/testFile.txt b/testFile.txt index 4c88aab13..6b9d30ab1 100644 --- a/testFile.txt +++ b/testFile.txt @@ -1,2 +1,4 @@ test file hmm +hmm +hmm diff --git a/testFile3.txt b/testFile3.txt index 1cf5b232c..3d8f5ddc6 100644 --- a/testFile3.txt +++ b/testFile3.txt @@ -1 +1,2 @@ hmm +hmm From 56f9dfd856e08ca0240486b258ea04b506e1419e Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:27:35 +1000 Subject: [PATCH 10/32] delete testFile2 --- testFile2.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 testFile2.txt diff --git a/testFile2.txt b/testFile2.txt deleted file mode 100644 index 3d8f5ddc6..000000000 --- a/testFile2.txt +++ /dev/null @@ -1,2 +0,0 @@ -hmm -hmm From f465a5fe48a20aea86476da5d4ce22dec32f9d92 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:29:24 +1000 Subject: [PATCH 11/32] Test File 4 --- testFile4.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 testFile4.txt diff --git a/testFile4.txt b/testFile4.txt new file mode 100644 index 000000000..3d8f5ddc6 --- /dev/null +++ b/testFile4.txt @@ -0,0 +1,2 @@ +hmm +hmm From 7d70ed5be1769a30ff7c5485b73561586a8f13b3 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 22:34:02 +1000 Subject: [PATCH 12/32] update --- gitcommands.go | 36 ++++++++-- gui.go | 184 ++++++++++++++++++++++++++++++++++++------------- main.go | 3 +- 3 files changed, 166 insertions(+), 57 deletions(-) diff --git a/gitcommands.go b/gitcommands.go index 465a61da1..87fd425c2 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -56,8 +56,10 @@ func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { return result } -func getGitBranchOutput() (string, error) { - cmdOut, err := exec.Command("bash", "-c", getBranchesCommand).Output() +func runDirectCommand(command string) (string, error) { + cmdOut, err := exec.Command("bash", "-c", command).Output() + devLog(string(cmdOut)) + devLog(fmt.Sprint(err)) return string(cmdOut), err } @@ -70,7 +72,7 @@ func branchNameFromString(branchString string) string { func getGitBranches() []Branch { branches := make([]Branch, 0) - rawString, _ := getGitBranchOutput() + rawString, _ := runDirectCommand(getBranchesCommand) branchLines := splitLines(rawString) for _, line := range branchLines { name := branchNameFromString(line) @@ -136,7 +138,7 @@ func runCommand(cmd string) (string, error) { splitCmd := strings.Split(cmd, " ") cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).Output() devLog(cmd) - devLog(string(cmdOut)) + devLog(string(cmdOut[:])) return string(cmdOut), err } @@ -150,10 +152,14 @@ func getDiff(file GitFile) string { cachedArg = "--cached " } deletedArg := "" - if file.Deleted || !file.Tracked { - deletedArg = "--no-index /dev/null " + if file.Deleted { + deletedArg = "-- " } - command := "git diff --color " + cachedArg + deletedArg + file.Name + trackedArg := "" + if !file.Tracked { + trackedArg = "--no-index /dev/null " + } + command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name s, err := runCommand(command) if err != nil { // for now we assume an error means the file was deleted @@ -176,6 +182,22 @@ func getGitStatus() (string, error) { return runCommand("git status --untracked-files=all --short") } +func removeFile(file GitFile) error { + // if the file isn't tracked, we assume you want to delete it + if !file.Tracked { + _, err := runCommand("rm -rf ./" + file.Name) + return err + } + // if the file is tracked, we assume you want to just check it out + _, err := runCommand("git checkout " + file.Name) + return err +} + +func gitCommit(message string) error { + _, err := runDirectCommand("git commit -m \"" + message + "\"") + return err +} + const getBranchesCommand = `set -e git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD | { seen=":" diff --git a/gui.go b/gui.go index 63566c737..289caf2d7 100644 --- a/gui.go +++ b/gui.go @@ -28,6 +28,16 @@ var state = stateType{GitFiles: make([]GitFile, 0)} var cyclableViews = []string{"files", "branches"} +func stagedFiles(files []GitFile) []GitFile { + result := make([]GitFile, 0) + for _, file := range files { + if file.HasStagedChanges { + result = append(result, file) + } + } + return result +} + func nextView(g *gocui.Gui, v *gocui.View) error { var focusedViewName string if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { @@ -48,21 +58,27 @@ func nextView(g *gocui.Gui, v *gocui.View) error { panic(err) return err } - if v != nil { - v.Highlight = false + return switchFocus(g, v, focusedView) +} + +func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { + if oldView != nil { + oldView.Highlight = false } - focusedView.Highlight = true - devLog(focusedViewName) - _, err = g.SetCurrentView(focusedViewName) - itemSelected(g, focusedView) - showViewOptions(g, focusedViewName) + newView.Highlight = true + devLog(newView.Name()) + _, err := g.SetCurrentView(newView.Name()) // not mega proud of the delayed + // return of err + itemSelected(g, newView) + showViewOptions(g, newView.Name()) return err } func showViewOptions(g *gocui.Gui, viewName string) error { optionsMap := map[string]string{ - "files": "space: toggle staged, c: commit changes", + "files": "space: toggle staged, c: commit changes, shift+d: remove", "branches": "space: checkout", + "prompt": "esc: cancel, enter: commit", } g.Update(func(*gocui.Gui) error { v, err := g.View("options") @@ -133,6 +149,8 @@ func itemSelected(g *gocui.Gui, v *gocui.View) error { return handleFileSelect(g, v) case "branches": return handleBranchSelect(g, v) + case "prompt": + return nil default: panic("No view matching itemSelected switch statement") } @@ -166,7 +184,7 @@ func devLog(s string) { func handleBranchPress(g *gocui.Gui, v *gocui.View) error { branch := getSelectedBranch(v) if err := gitCheckout(branch.Name, false); err != nil { - return err + panic(err) } refreshBranches(v) refreshFiles(g) @@ -192,8 +210,67 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error { return nil } +func handleCommitPrompt(g *gocui.Gui, currentView *gocui.View) error { + devLog(fmt.Sprint(stagedFiles(state.GitFiles))) + if len(stagedFiles(state.GitFiles)) == 0 { + return nil + } + maxX, maxY := g.Size() + // var v *gocui.View + if v, err := g.SetView("prompt", maxX/2-30, maxY/2-1, maxX/2+30, maxY/2+1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Commit Message: " + v.Editable = true + v.Highlight = true + v.Autoscroll = true + v.Wrap = true + v.Overwrite = true + v.Caret = true + // fmt.Fprintln(v, "commit message: ") + if _, err := g.SetCurrentView("prompt"); err != nil { + return err + } + switchFocus(g, currentView, v) + } + return nil +} + +func handleCommitSubmit(g *gocui.Gui, v *gocui.View) error { + if len(v.BufferLines()) == 0 { + return closePrompt(g, v) + } + message := fmt.Sprint(v.BufferLines()[0]) + // for whatever reason, a successful commit returns an error, so we're not + // going to check for an error here + if err := gitCommit(message); err != nil { + devLog(fmt.Sprint(err)) + panic(err) + } + refreshFiles(g) + return closePrompt(g, v) +} + +func handleFileRemove(g *gocui.Gui, v *gocui.View) error { + file := getSelectedFile(v) + removeFile(file) + refreshFiles(g) + return nil +} + func getSelectedFile(v *gocui.View) GitFile { lineNumber := getItemPosition(v) + if len(state.GitFiles) == 0 { + return GitFile{ + Name: "noFile", + DisplayString: "none", + HasStagedChanges: false, + HasUnstagedChanges: false, + Tracked: false, + Deleted: false, + } + } return state.GitFiles[lineNumber] } @@ -215,25 +292,14 @@ func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { func handleFileSelect(g *gocui.Gui, v *gocui.View) error { item := getSelectedFile(v) diff := getDiff(item) - if err := renderString(g, diff); err != nil { - return err - } - - // maxX, maxY := g.Size() - // if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil { - // if err != gocui.ErrUnknownView { - // return errkjhgkhj - // } - // fmt.Fprintln(v, l) - // if _, err := g.SetCurrentView("msg"); err != nil { - // return err - // } - // } - return nil + return renderString(g, diff) } -func delMsg(g *gocui.Gui, v *gocui.View) error { - if err := g.DeleteView("msg"); err != nil { +func closePrompt(g *gocui.Gui, v *gocui.View) error { + filesView, _ := g.View("files") + switchFocus(g, v, filesView) + devLog("test prompt close") + if err := g.DeleteView("prompt"); err != nil { return err } if _, err := g.SetCurrentView("files"); err != nil { @@ -247,32 +313,42 @@ func quit(g *gocui.Gui, v *gocui.View) error { } func keybindings(g *gocui.Gui) error { - for _, view := range cyclableViews { - if err := g.SetKeybinding(view, gocui.KeyTab, gocui.ModNone, nextView); err != nil { - return err - } - if err := g.SetKeybinding(view, 'q', gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding(view, gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding(view, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { - return err - } - if err := g.SetKeybinding(view, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { - return err - } - if err := g.SetKeybinding(view, gocui.KeyPgup, gocui.ModNone, scrollUp); err != nil { - return err - } - if err := g.SetKeybinding(view, gocui.KeyPgdn, gocui.ModNone, scrollDown); err != nil { - return err - } + if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { + return err + } + if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, scrollUp); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDown); err != nil { + return err + } + if err := g.SetKeybinding("", 'C', gocui.ModNone, handleCommitPrompt); err != nil { + return err } if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil { return err } + if err := g.SetKeybinding("files", 'D', gocui.ModNone, handleFileRemove); err != nil { + return err + } + if err := g.SetKeybinding("prompt", gocui.KeyEsc, gocui.ModNone, closePrompt); err != nil { + return err + } + if err := g.SetKeybinding("prompt", gocui.KeyEnter, gocui.ModNone, handleCommitSubmit); err != nil { + return err + } if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { return err } @@ -317,6 +393,17 @@ func refreshBranches(v *gocui.View) error { return nil } +// if the cursor down past the last item, move it up one +func correctCursor(v *gocui.View) error { + cx, cy := v.Cursor() + _, oy := v.Origin() + lineCount := len(v.BufferLines()) - 2 + if cy >= lineCount-oy { + return v.SetCursor(cx, lineCount-oy) + } + return nil +} + func refreshFiles(g *gocui.Gui) error { filesView, err := g.View("files") if err != nil { @@ -343,6 +430,7 @@ func refreshFiles(g *gocui.Gui) error { green.Fprintln(filesView, gitFile.Name) } } + correctCursor(filesView) return nil } diff --git a/main.go b/main.go index a83209ef1..15848b980 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main func main() { + devLog("\n\n\n\n\n\n\n\n\n\n") run() } - - From 796286e98e529cf6f767a7df3449f6fcfd199bfd Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 23:01:41 +1000 Subject: [PATCH 13/32] Another test --- testFile2.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 testFile2.txt diff --git a/testFile2.txt b/testFile2.txt new file mode 100644 index 000000000..1cf5b232c --- /dev/null +++ b/testFile2.txt @@ -0,0 +1 @@ +hmm From 301d34ef3d6ef180c59b3d01907188a073dc040b Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 23:03:06 +1000 Subject: [PATCH 14/32] Test --- testFile4.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/testFile4.txt b/testFile4.txt index 3d8f5ddc6..ffb448706 100644 --- a/testFile4.txt +++ b/testFile4.txt @@ -1,2 +1,3 @@ hmm hmm +hmm From d62f41a4f1d91479d30020e354cefd32931ea38d Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 21 May 2018 23:04:10 +1000 Subject: [PATCH 15/32] TEst --- testFile3.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/testFile3.txt b/testFile3.txt index 3d8f5ddc6..ffb448706 100644 --- a/testFile3.txt +++ b/testFile3.txt @@ -1,2 +1,3 @@ hmm hmm +hmm From fe99e70983b80346aa76420da0461847b879f105 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 22 May 2018 10:51:54 +1000 Subject: [PATCH 16/32] hkghghjg --- notes/go.notes | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 notes/go.notes diff --git a/notes/go.notes b/notes/go.notes new file mode 100644 index 000000000..1e0c0b8d0 --- /dev/null +++ b/notes/go.notes @@ -0,0 +1,78 @@ +TODO: + +committing +blowing away files: + if it's untracked, delete it + if it's tracked, check it out + + + +----------------------------------------------------------- + GO +----------------------------------------------------------- + +Running and Building: + +$ go run hello-world.go +hello world + +$ go build hello-world.go +$ ls +hello-world hello-world.go + +$ ./hello-world +hello world + +----------------------------------------------------------- + DIRECTORY STRUCTURE +----------------------------------------------------------- + +https://golang.org/doc/code.html + +if you don't have your GOPATH exported, do so with +export GOPATH=$(go env GOPATH) + +you have a GOPATH which points to e.g. ~/go/ +this is where everything in go is stored. + +it has three directories, + - src + - pkg + - bin + +installed programs have their executables stored in bin +all your project and the src code of other people's projects are in src, with paths identifying them e.g. +src/github.com/jesseduffield/gitgot + +If you want to make an executable, give every file in your project directory `package main`, otherwise if you want to make a package, name everything `package mypackage`. This name should be the name of the project directory for your project. + +to install a program, inside the project folder +go install + +This adds the program to /bin + +to build a package, inside the project folder +go build + +this adds the package to /pkg so that it can be linked easily in the future. + +to build and run your program just do this: + +~/github.com/jesseduffield/gitgot: +▶ go install && gitgot + +----------------------------------------------------------- + BUILD SYSTEMS +----------------------------------------------------------- + +Currently for installs I'm using +/Users/jesseduffieldduffield/Library/Application Support/Sublime Text 3/Packages/User/custom_go_install.sublime-build + +note that the argument to go install should be the directory from the end of /src/ onwards. + +----------------------------------------------------------- + FMT +----------------------------------------------------------- + +casting anything to a string +fmt.Sprint(thing) From bb12309c7c834f8b0b12d7942cfee0d071242ad0 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 26 May 2018 13:23:39 +1000 Subject: [PATCH 17/32] Breaking into different files --- .gitignore | 1 + commit_panel.go | 67 +++++++ confirmation_panel.go | 62 +++++++ files_panel.go | 136 ++++++++++++++ gitcommands.go | 57 +++++- gui.go | 407 ++++++------------------------------------ logs_panel.go | 30 ++++ view_helpers.go | 120 +++++++++++++ 8 files changed, 523 insertions(+), 357 deletions(-) create mode 100644 commit_panel.go create mode 100644 confirmation_panel.go create mode 100644 files_panel.go create mode 100644 logs_panel.go create mode 100644 view_helpers.go diff --git a/.gitignore b/.gitignore index 1032e298f..48dffc2a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ development.log +commands.log diff --git a/commit_panel.go b/commit_panel.go new file mode 100644 index 000000000..17f54882a --- /dev/null +++ b/commit_panel.go @@ -0,0 +1,67 @@ +// 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 handleCommitPress(g *gocui.Gui, currentView *gocui.View) error { + devLog(stagedFiles(state.GitFiles)) + if len(stagedFiles(state.GitFiles)) == 0 { + return nil + } + maxX, maxY := g.Size() + // var v *gocui.View + if v, err := g.SetView("commit", maxX/2-30, maxY/2-1, maxX/2+30, maxY/2+1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Commit Message" + v.Editable = true + if _, err := g.SetCurrentView("commit"); err != nil { + return err + } + switchFocus(g, currentView, v) + } + return nil +} + +func handleCommitSubmit(g *gocui.Gui, v *gocui.View) error { + if len(v.BufferLines()) == 0 { + return closeCommitPrompt(g, v) + } + message := fmt.Sprint(v.BufferLines()[0]) + // for whatever reason, a successful commit returns an error, so we're not + // going to check for an error here + if err := gitCommit(message); err != nil { + devLog(err) + panic(err) + } + refreshFiles(g) + refreshLogs(g) + return closeCommitPrompt(g, v) +} + +func closeCommitPrompt(g *gocui.Gui, v *gocui.View) error { + filesView, _ := g.View("files") + switchFocus(g, v, filesView) + devLog("test prompt close") + if err := g.DeleteView("commit"); err != nil { + return err + } + if _, err := g.SetCurrentView(state.PreviousView); err != nil { + return err + } + return nil +} + +func handleCommitPromptFocus(g *gocui.Gui, v *gocui.View) error { + return renderString(g, "options", "esc: close, enter: commit") +} diff --git a/confirmation_panel.go b/confirmation_panel.go new file mode 100644 index 000000000..b63534987 --- /dev/null +++ b/confirmation_panel.go @@ -0,0 +1,62 @@ +// 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 ( + + // "io" + // "io/ioutil" + + "math" + // "strings" + + "github.com/jroimartin/gocui" +) + +func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error { + return func(g *gocui.Gui, v *gocui.View) error { + if function != nil { + if err := function(g, v); err != nil { + panic(err) + } + } + if err := returnFocus(g, v); err != nil { + panic(err) + } + g.DeleteKeybindings("confirmation") + return g.DeleteView("confirmation") + } +} + +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))) + 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 { + 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 { + return err + } + if err := g.SetKeybinding("confirmation", 'y', gocui.ModNone, wrappedConfirmationFunction(handleYes)); err != nil { + return err + } + } + return nil +} diff --git a/files_panel.go b/files_panel.go new file mode 100644 index 000000000..40c983481 --- /dev/null +++ b/files_panel.go @@ -0,0 +1,136 @@ +// 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 ( + + // "io" + // "io/ioutil" + + // "strings" + + "strings" + + "github.com/fatih/color" + "github.com/jroimartin/gocui" +) + +func stagedFiles(files []GitFile) []GitFile { + result := make([]GitFile, 0) + for _, file := range files { + if file.HasStagedChanges { + result = append(result, file) + } + } + return result +} + +func handleFilePress(g *gocui.Gui, v *gocui.View) error { + file := getSelectedFile(v) + + if file.HasUnstagedChanges { + stageFile(file.Name) + } else { + unStageFile(file.Name) + } + + if err := refreshFiles(g); err != nil { + return err + } + if err := handleFileSelect(g, v); err != nil { + return err + } + + return nil +} + +func getSelectedFile(v *gocui.View) GitFile { + lineNumber := getItemPosition(v) + if len(state.GitFiles) == 0 { + return GitFile{ + Name: "noFile", + DisplayString: "none", + HasStagedChanges: false, + HasUnstagedChanges: false, + Tracked: false, + Deleted: false, + } + } + return state.GitFiles[lineNumber] +} + +func handleFileRemove(g *gocui.Gui, v *gocui.View) error { + file := getSelectedFile(v) + var deleteVerb string + if file.Tracked { + deleteVerb = "checkout" + } 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 { + if err := removeFile(file); err != nil { + panic(err) + } + return refreshFiles(g) + }, nil) +} + +func handleFileSelect(g *gocui.Gui, v *gocui.View) error { + item := getSelectedFile(v) + var optionsString string + baseString := "space: toggle staged, c: commit changes, option+o: open" + if item.Tracked { + optionsString = baseString + ", option+d: checkout" + } else { + optionsString = baseString + ", option+d: delete" + } + renderString(g, "options", optionsString) + diff := getDiff(item) + return renderString(g, "main", diff) +} + +func handleFileOpen(g *gocui.Gui, v *gocui.View) error { + file := getSelectedFile(v) + _, err := openFile(file.Name) + return err +} + +func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { + file := getSelectedFile(v) + _, err := sublimeOpenFile(file.Name) + return err +} + +func refreshFiles(g *gocui.Gui) error { + filesView, err := g.View("files") + if err != nil { + return err + } + + // get files to stage + gitFiles := getGitStatusFiles() + state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) + + filesView.Clear() + red := color.New(color.FgRed) + green := color.New(color.FgGreen) + for _, gitFile := range state.GitFiles { + if !gitFile.Tracked { + red.Fprintln(filesView, gitFile.DisplayString) + continue + } + green.Fprint(filesView, gitFile.DisplayString[0:1]) + red.Fprint(filesView, gitFile.DisplayString[1:3]) + if gitFile.HasUnstagedChanges { + red.Fprintln(filesView, gitFile.Name) + } else { + green.Fprintln(filesView, gitFile.Name) + } + } + correctCursor(filesView) + return nil +} diff --git a/gitcommands.go b/gitcommands.go index 87fd425c2..fbfded157 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -8,6 +8,7 @@ import ( // "log" "fmt" + "os" "os/exec" "strings" ) @@ -30,6 +31,23 @@ type Branch struct { BaseBranch string } +func devLog(objects ...interface{}) { + localLog("/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...) +} + +func localLog(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") + } +} + // Map (from https://gobyexample.com/collection-functions) func Map(vs []string, f func(string) string) []string { vsm := make([]string, len(vs)) @@ -39,6 +57,15 @@ func Map(vs []string, f func(string) string) []string { return vsm } +func includes(list []string, a string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { if len(oldGitFiles) == 0 { return newGitFiles @@ -57,9 +84,10 @@ func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { } func runDirectCommand(command string) (string, error) { + commandLog(command) cmdOut, err := exec.Command("bash", "-c", command).Output() devLog(string(cmdOut)) - devLog(fmt.Sprint(err)) + devLog(err) return string(cmdOut), err } @@ -74,7 +102,7 @@ func getGitBranches() []Branch { branches := make([]Branch, 0) rawString, _ := runDirectCommand(getBranchesCommand) branchLines := splitLines(rawString) - for _, line := range branchLines { + for i, line := range branchLines { name := branchNameFromString(line) var branchType string var baseBranch string @@ -91,16 +119,19 @@ func getGitBranches() []Branch { branchType = "other" baseBranch = name } + if i == 0 { + line = line[:2] + "\t*" + line[2:] + } branches = append(branches, Branch{name, line, branchType, baseBranch}) } - devLog(fmt.Sprint(branches)) + devLog(branches) return branches } func getGitStatusFiles() []GitFile { statusOutput, _ := getGitStatus() statusStrings := splitLines(statusOutput) - devLog(fmt.Sprint(statusStrings)) + devLog(statusStrings) // a file can have both staged and unstaged changes // I'll probably end up ignoring the unstaged flag for now but might revisit // tracked, staged, unstaged @@ -135,17 +166,33 @@ func gitCheckout(branch string, force bool) error { } func runCommand(cmd string) (string, error) { + commandLog(cmd) splitCmd := strings.Split(cmd, " ") cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).Output() - devLog(cmd) devLog(string(cmdOut[:])) return string(cmdOut), err } +func openFile(filename string) (string, error) { + return runCommand("open " + filename) +} + +func sublimeOpenFile(filename string) (string, error) { + return runCommand("subl " + filename) +} + func getBranchDiff(branch string, baseBranch string) (string, error) { return runCommand("git diff --color " + baseBranch + "..." + branch) } +func getLog() string { + result, err := runDirectCommand("git log --color --oneline") + if err != nil { + panic(err) + } + return result +} + func getDiff(file GitFile) string { cachedArg := "" if file.HasStagedChanges { diff --git a/gui.go b/gui.go index 289caf2d7..62e77ff26 100644 --- a/gui.go +++ b/gui.go @@ -7,37 +7,29 @@ package main import ( - "fmt" - "strings" + // "io" // "io/ioutil" + "log" // "strings" - "os" - "github.com/fatih/color" "github.com/jroimartin/gocui" ) type stateType struct { - GitFiles []GitFile - Branches []Branch + GitFiles []GitFile + Branches []Branch + PreviousView string } -var state = stateType{GitFiles: make([]GitFile, 0)} +var state = stateType{ + GitFiles: make([]GitFile, 0), + PreviousView: "files", +} var cyclableViews = []string{"files", "branches"} -func stagedFiles(files []GitFile) []GitFile { - result := make([]GitFile, 0) - for _, file := range files { - if file.HasStagedChanges { - result = append(result, file) - } - } - return result -} - func nextView(g *gocui.Gui, v *gocui.View) error { var focusedViewName string if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { @@ -61,86 +53,7 @@ func nextView(g *gocui.Gui, v *gocui.View) error { return switchFocus(g, v, focusedView) } -func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { - if oldView != nil { - oldView.Highlight = false - } - newView.Highlight = true - devLog(newView.Name()) - _, err := g.SetCurrentView(newView.Name()) // not mega proud of the delayed - // return of err - itemSelected(g, newView) - showViewOptions(g, newView.Name()) - return err -} - -func showViewOptions(g *gocui.Gui, viewName string) error { - optionsMap := map[string]string{ - "files": "space: toggle staged, c: commit changes, shift+d: remove", - "branches": "space: checkout", - "prompt": "esc: cancel, enter: commit", - } - g.Update(func(*gocui.Gui) error { - v, err := g.View("options") - if err != nil { - panic(err) - } - v.Clear() - fmt.Fprint(v, optionsMap[viewName]) - return nil - }) - return nil -} - -func getItemPosition(v *gocui.View) int { - _, cy := v.Cursor() - _, oy := v.Origin() - return oy + cy -} - -func cursorUp(g *gocui.Gui, v *gocui.View) error { - if v == nil { - return nil - } - - ox, oy := v.Origin() - cx, cy := v.Cursor() - if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { - if err := v.SetOrigin(ox, oy-1); err != nil { - return err - } - } - - itemSelected(g, v) - return nil -} - -func resetOrigin(v *gocui.View) error { - if err := v.SetCursor(0, 0); err != nil { - return err - } - return v.SetOrigin(0, 0) -} - -func cursorDown(g *gocui.Gui, v *gocui.View) error { - if v != nil { - cx, cy := v.Cursor() - ox, oy := v.Origin() - if cy+oy >= len(v.BufferLines())-2 { - return nil - } - if err := v.SetCursor(cx, cy+1); err != nil { - if err := v.SetOrigin(ox, oy+1); err != nil { - return err - } - } - } - - itemSelected(g, v) - return nil -} - -func itemSelected(g *gocui.Gui, v *gocui.View) error { +func newLineFocused(g *gocui.Gui, v *gocui.View) error { mainView, _ := g.View("main") mainView.SetOrigin(0, 0) @@ -149,14 +62,18 @@ func itemSelected(g *gocui.Gui, v *gocui.View) error { return handleFileSelect(g, v) case "branches": return handleBranchSelect(g, v) - case "prompt": + case "commit": + return handleCommitPromptFocus(g, v) + case "confirmation": + return nil + case "main": return nil default: - panic("No view matching itemSelected switch statement") + panic("No view matching newLineFocused switch statement") } } -func scrollUp(g *gocui.Gui, v *gocui.View) error { +func scrollUpMain(g *gocui.Gui, v *gocui.View) error { mainView, _ := g.View("main") ox, oy := mainView.Origin() if oy >= 1 { @@ -165,7 +82,7 @@ func scrollUp(g *gocui.Gui, v *gocui.View) error { return nil } -func scrollDown(g *gocui.Gui, v *gocui.View) error { +func scrollDownMain(g *gocui.Gui, v *gocui.View) error { mainView, _ := g.View("main") ox, oy := mainView.Origin() if oy < len(mainView.BufferLines()) { @@ -174,144 +91,6 @@ func scrollDown(g *gocui.Gui, v *gocui.View) error { return nil } -func devLog(s string) { - f, _ := os.OpenFile("/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", os.O_APPEND|os.O_WRONLY, 0644) - defer f.Close() - - f.WriteString(s + "\n") -} - -func handleBranchPress(g *gocui.Gui, v *gocui.View) error { - branch := getSelectedBranch(v) - if err := gitCheckout(branch.Name, false); err != nil { - panic(err) - } - refreshBranches(v) - refreshFiles(g) - return nil -} - -func handleFilePress(g *gocui.Gui, v *gocui.View) error { - file := getSelectedFile(v) - - if file.HasUnstagedChanges { - stageFile(file.Name) - } else { - unStageFile(file.Name) - } - - if err := refreshFiles(g); err != nil { - return err - } - if err := handleFileSelect(g, v); err != nil { - return err - } - - return nil -} - -func handleCommitPrompt(g *gocui.Gui, currentView *gocui.View) error { - devLog(fmt.Sprint(stagedFiles(state.GitFiles))) - if len(stagedFiles(state.GitFiles)) == 0 { - return nil - } - maxX, maxY := g.Size() - // var v *gocui.View - if v, err := g.SetView("prompt", maxX/2-30, maxY/2-1, maxX/2+30, maxY/2+1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Commit Message: " - v.Editable = true - v.Highlight = true - v.Autoscroll = true - v.Wrap = true - v.Overwrite = true - v.Caret = true - // fmt.Fprintln(v, "commit message: ") - if _, err := g.SetCurrentView("prompt"); err != nil { - return err - } - switchFocus(g, currentView, v) - } - return nil -} - -func handleCommitSubmit(g *gocui.Gui, v *gocui.View) error { - if len(v.BufferLines()) == 0 { - return closePrompt(g, v) - } - message := fmt.Sprint(v.BufferLines()[0]) - // for whatever reason, a successful commit returns an error, so we're not - // going to check for an error here - if err := gitCommit(message); err != nil { - devLog(fmt.Sprint(err)) - panic(err) - } - refreshFiles(g) - return closePrompt(g, v) -} - -func handleFileRemove(g *gocui.Gui, v *gocui.View) error { - file := getSelectedFile(v) - removeFile(file) - refreshFiles(g) - return nil -} - -func getSelectedFile(v *gocui.View) GitFile { - lineNumber := getItemPosition(v) - if len(state.GitFiles) == 0 { - return GitFile{ - Name: "noFile", - DisplayString: "none", - HasStagedChanges: false, - HasUnstagedChanges: false, - Tracked: false, - Deleted: false, - } - } - return state.GitFiles[lineNumber] -} - -func getSelectedBranch(v *gocui.View) Branch { - lineNumber := getItemPosition(v) - return state.Branches[lineNumber] -} - -func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { - lineNumber := getItemPosition(v) - branch := state.Branches[lineNumber] - diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) - if err := renderString(g, diff); err != nil { - return err - } - return nil -} - -func handleFileSelect(g *gocui.Gui, v *gocui.View) error { - item := getSelectedFile(v) - diff := getDiff(item) - return renderString(g, diff) -} - -func closePrompt(g *gocui.Gui, v *gocui.View) error { - filesView, _ := g.View("files") - switchFocus(g, v, filesView) - devLog("test prompt close") - if err := g.DeleteView("prompt"); err != nil { - return err - } - if _, err := g.SetCurrentView("files"); err != nil { - return err - } - return nil -} - -func quit(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit -} - func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { return err @@ -328,25 +107,31 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { return err } - if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, scrollUp); err != nil { + if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, scrollUpMain); err != nil { return err } - if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDown); err != nil { + if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil { return err } - if err := g.SetKeybinding("", 'C', gocui.ModNone, handleCommitPrompt); err != nil { + if err := g.SetKeybinding("", 'ç', 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", 'D', gocui.ModNone, handleFileRemove); err != nil { + if err := g.SetKeybinding("files", '®', gocui.ModNone, handleFileRemove); err != nil { return err } - if err := g.SetKeybinding("prompt", gocui.KeyEsc, gocui.ModNone, closePrompt); err != nil { + if err := g.SetKeybinding("files", 'ø', gocui.ModNone, handleFileOpen); err != nil { return err } - if err := g.SetKeybinding("prompt", gocui.KeyEnter, gocui.ModNone, handleCommitSubmit); err != nil { + if err := g.SetKeybinding("files", 'ß', gocui.ModNone, handleSublimeFileOpen); 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 { return err } if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { @@ -355,94 +140,16 @@ func keybindings(g *gocui.Gui) error { return nil } -func splitLines(multilineString string) []string { - if multilineString == "" || multilineString == "\n" { - return make([]string, 0) - } - lines := strings.Split(multilineString, "\n") - if lines[len(lines)-1] == "" { - return lines[:len(lines)-1] - } - return lines -} - -func refreshBranches(v *gocui.View) error { - state.Branches = getGitBranches() - yellow := color.New(color.FgYellow) - red := color.New(color.FgRed) - white := color.New(color.FgWhite) - green := color.New(color.FgGreen) - - v.Clear() - for _, branch := range state.Branches { - if branch.Type == "feature" { - green.Fprintln(v, branch.DisplayString) - continue - } - if branch.Type == "bugfix" { - yellow.Fprintln(v, branch.DisplayString) - continue - } - if branch.Type == "hotfix" { - red.Fprintln(v, branch.DisplayString) - continue - } - white.Fprintln(v, branch.DisplayString) - } - resetOrigin(v) - return nil -} - -// if the cursor down past the last item, move it up one -func correctCursor(v *gocui.View) error { - cx, cy := v.Cursor() - _, oy := v.Origin() - lineCount := len(v.BufferLines()) - 2 - if cy >= lineCount-oy { - return v.SetCursor(cx, lineCount-oy) - } - return nil -} - -func refreshFiles(g *gocui.Gui) error { - filesView, err := g.View("files") - if err != nil { - return err - } - - // get files to stage - gitFiles := getGitStatusFiles() - state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) - - filesView.Clear() - red := color.New(color.FgRed) - green := color.New(color.FgGreen) - for _, gitFile := range state.GitFiles { - if !gitFile.Tracked { - red.Fprintln(filesView, gitFile.DisplayString) - continue - } - green.Fprint(filesView, gitFile.DisplayString[0:1]) - red.Fprint(filesView, gitFile.DisplayString[1:3]) - if gitFile.HasUnstagedChanges { - red.Fprintln(filesView, gitFile.Name) - } else { - green.Fprintln(filesView, gitFile.Name) - } - } - correctCursor(filesView) - return nil -} - func layout(g *gocui.Gui) error { - maxX, maxY := g.Size() - leftSideWidth := maxX / 3 - filesBranchesBoundary := maxY - 10 + width, height := g.Size() + leftSideWidth := width / 3 + logsBranchesBoundary := height - 10 + filesBranchesBoundary := height - 20 - optionsTop := maxY - 3 + optionsTop := height - 3 // hiding options if there's not enough space - if maxY < 30 { - optionsTop = maxY + if height < 30 { + optionsTop = height } sideView, err := g.SetView("files", 0, 0, leftSideWidth, filesBranchesBoundary-1) @@ -455,19 +162,27 @@ func layout(g *gocui.Gui) error { refreshFiles(g) } - if v, err := g.SetView("main", leftSideWidth+2, 0, maxX-1, optionsTop-1); err != nil { + if v, err := g.SetView("main", leftSideWidth+2, 0, width-1, optionsTop-1); err != nil { if err != gocui.ErrUnknownView { return err } v.Title = "Diff" v.Wrap = true - if _, err := g.SetCurrentView("files"); err != nil { - return err - } + switchFocus(g, nil, v) handleFileSelect(g, sideView) } - if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, optionsTop-1); err != nil { + if v, err := g.SetView("logs", 0, logsBranchesBoundary, leftSideWidth, optionsTop-1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Log" + + // these are only called once + refreshLogs(g) + } + + if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, logsBranchesBoundary-1); err != nil { if err != gocui.ErrUnknownView { return err } @@ -478,7 +193,7 @@ func layout(g *gocui.Gui) error { nextView(g, nil) } - if v, err := g.SetView("options", 0, optionsTop, maxX-1, optionsTop+2); err != nil { + if v, err := g.SetView("options", 0, optionsTop, width-1, optionsTop+2); err != nil { if err != gocui.ErrUnknownView { return err } @@ -488,20 +203,6 @@ func layout(g *gocui.Gui) error { return nil } -func renderString(g *gocui.Gui, s string) error { - g.Update(func(*gocui.Gui) error { - v, err := g.View("main") - if err != nil { - panic(err) - } - v.Clear() - fmt.Fprint(v, s) - v.Wrap = true - return nil - }) - return nil -} - func run() { g, err := gocui.NewGui(gocui.OutputNormal) if err != nil { @@ -509,8 +210,6 @@ func run() { } defer g.Close() - // g.Cursor = true - g.SetManagerFunc(layout) if err := keybindings(g); err != nil { @@ -522,6 +221,10 @@ func run() { } } +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + // const mcRide = " // `.-::-` // -/o+oossys+:. diff --git a/logs_panel.go b/logs_panel.go new file mode 100644 index 000000000..aa6f1ce48 --- /dev/null +++ b/logs_panel.go @@ -0,0 +1,30 @@ +// 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/view_helpers.go b/view_helpers.go new file mode 100644 index 000000000..2306dd2f7 --- /dev/null +++ b/view_helpers.go @@ -0,0 +1,120 @@ +// 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" + "strings" + + "github.com/jroimartin/gocui" +) + +func returnFocus(g *gocui.Gui, v *gocui.View) error { + previousView, err := g.View(state.PreviousView) + if err != nil { + panic(err) + } + return switchFocus(g, v, previousView) +} + +func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { + if oldView != nil { + oldView.Highlight = false + state.PreviousView = oldView.Name() + } + newView.Highlight = true + devLog(newView.Name()) + if _, err := g.SetCurrentView(newView.Name()); err != nil { + return err + } + g.Cursor = newView.Name() == "commit" + return newLineFocused(g, newView) +} + +func getItemPosition(v *gocui.View) int { + _, cy := v.Cursor() + _, oy := v.Origin() + return oy + cy +} + +func cursorUp(g *gocui.Gui, v *gocui.View) error { + if v == nil { + return nil + } + + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + + newLineFocused(g, v) + return nil +} + +func resetOrigin(v *gocui.View) error { + if err := v.SetCursor(0, 0); err != nil { + return err + } + return v.SetOrigin(0, 0) +} + +func cursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + ox, oy := v.Origin() + if cy+oy >= len(v.BufferLines())-2 { + return nil + } + if err := v.SetCursor(cx, cy+1); err != nil { + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + } + + newLineFocused(g, v) + return nil +} + +// if the cursor down past the last item, move it up one +func correctCursor(v *gocui.View) error { + cx, cy := v.Cursor() + _, oy := v.Origin() + lineCount := len(v.BufferLines()) - 2 + if cy >= lineCount-oy { + return v.SetCursor(cx, lineCount-oy) + } + return nil +} + +func renderString(g *gocui.Gui, viewName, s string) error { + g.Update(func(*gocui.Gui) error { + v, err := g.View(viewName) + if err != nil { + panic(err) + } + v.Clear() + fmt.Fprint(v, s) + v.Wrap = true + return nil + }) + return nil +} + +func splitLines(multilineString string) []string { + if multilineString == "" || multilineString == "\n" { + return make([]string, 0) + } + lines := strings.Split(multilineString, "\n") + if lines[len(lines)-1] == "" { + return lines[:len(lines)-1] + } + return lines +} From 10fd353a50c894dc8e5bb68c0b939be63846a6c0 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 26 May 2018 15:44:44 +1000 Subject: [PATCH 18/32] More stuff --- branches_panel.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++ files_panel.go | 8 ++++++ gitcommands.go | 5 ++++ 3 files changed, 85 insertions(+) create mode 100644 branches_panel.go diff --git a/branches_panel.go b/branches_panel.go new file mode 100644 index 000000000..bb96947aa --- /dev/null +++ b/branches_panel.go @@ -0,0 +1,72 @@ +// 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 ( + + // "io" + // "io/ioutil" + + // "strings" + + "github.com/fatih/color" + "github.com/jroimartin/gocui" +) + +func handleBranchPress(g *gocui.Gui, v *gocui.View) error { + branch := getSelectedBranch(v) + if err := gitCheckout(branch.Name, false); err != nil { + panic(err) + } + refreshBranches(v) + refreshFiles(g) + refreshLogs(g) + return nil +} + +func getSelectedBranch(v *gocui.View) Branch { + lineNumber := getItemPosition(v) + return state.Branches[lineNumber] +} + +func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { + renderString(g, "options", "space: checkout") + lineNumber := getItemPosition(v) + branch := state.Branches[lineNumber] + diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) + if err := renderString(g, "main", diff); err != nil { + return err + } + return nil +} + +func refreshBranches(v *gocui.View) error { + state.Branches = getGitBranches() + yellow := color.New(color.FgYellow) + red := color.New(color.FgRed) + white := color.New(color.FgWhite) + green := color.New(color.FgGreen) + + v.Clear() + for _, branch := range state.Branches { + if branch.Type == "feature" { + green.Fprintln(v, branch.DisplayString) + continue + } + if branch.Type == "bugfix" { + yellow.Fprintln(v, branch.DisplayString) + continue + } + if branch.Type == "hotfix" { + red.Fprintln(v, branch.DisplayString) + continue + } + white.Fprintln(v, branch.DisplayString) + } + resetOrigin(v) + return nil +} diff --git a/files_panel.go b/files_panel.go index 40c983481..8820ee145 100644 --- a/files_panel.go +++ b/files_panel.go @@ -134,3 +134,11 @@ func refreshFiles(g *gocui.Gui) error { correctCursor(filesView) return nil } + +func pullFiles(g *gocui.Gui, v *gocui.Gui) error { + if err := gitPull(); err != nil { + // should show error + panic(err) + } + return refreshFiles(g) +} diff --git a/gitcommands.go b/gitcommands.go index fbfded157..b4fca3835 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -245,6 +245,11 @@ func gitCommit(message string) error { return err } +func gitPull() error { + _, err := runDirectCommand("git pull --no-edit") + return err +} + const getBranchesCommand = `set -e git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD | { seen=":" From 00dca6f76d406081bb8219c44534d4ff1de417d1 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 26 May 2018 15:47:22 +1000 Subject: [PATCH 19/32] sending panel to back rather than deleting --- commit_panel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commit_panel.go b/commit_panel.go index 17f54882a..5652cd59b 100644 --- a/commit_panel.go +++ b/commit_panel.go @@ -53,7 +53,7 @@ func closeCommitPrompt(g *gocui.Gui, v *gocui.View) error { filesView, _ := g.View("files") switchFocus(g, v, filesView) devLog("test prompt close") - if err := g.DeleteView("commit"); err != nil { + if _, err := g.SetViewOnBottom("commit"); err != nil { return err } if _, err := g.SetCurrentView(state.PreviousView); err != nil { From 112893f73c1b703d89a839cb45eb5542f33ef904 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 26 May 2018 15:51:28 +1000 Subject: [PATCH 20/32] reverting to using delete --- commit_panel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commit_panel.go b/commit_panel.go index 5652cd59b..17f54882a 100644 --- a/commit_panel.go +++ b/commit_panel.go @@ -53,7 +53,7 @@ func closeCommitPrompt(g *gocui.Gui, v *gocui.View) error { filesView, _ := g.View("files") switchFocus(g, v, filesView) devLog("test prompt close") - if _, err := g.SetViewOnBottom("commit"); err != nil { + if err := g.DeleteView("commit"); err != nil { return err } if _, err := g.SetCurrentView(state.PreviousView); err != nil { From 06713f608bf510e6157a3b73203ee621b48de6cd Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 26 May 2018 16:08:08 +1000 Subject: [PATCH 21/32] fixing up commit message deletion --- gui.go | 9 +++++++++ view_helpers.go | 2 ++ 2 files changed, 11 insertions(+) diff --git a/gui.go b/gui.go index 62e77ff26..08bbe8fbe 100644 --- a/gui.go +++ b/gui.go @@ -137,6 +137,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("", '∑', gocui.ModNone, handleLogState); err != nil { + return err + } + return nil +} + +func handleLogState(g *gocui.Gui, v *gocui.View) error { + devLog("state is:", state) + devLog("previous view:", state.PreviousView) return nil } diff --git a/view_helpers.go b/view_helpers.go index 2306dd2f7..01af1cdeb 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -24,7 +24,9 @@ func returnFocus(g *gocui.Gui, v *gocui.View) error { func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { if oldView != nil { oldView.Highlight = false + devLog("setting previous view to ") state.PreviousView = oldView.Name() + devLog(state.PreviousView) } newView.Highlight = true devLog(newView.Name()) From 843927dd26348f5dd49bad8b2ab6b4c90147461e Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 26 May 2018 16:08:18 +1000 Subject: [PATCH 22/32] more fixing --- commit_panel.go | 5 +++-- view_helpers.go | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/commit_panel.go b/commit_panel.go index 17f54882a..5de77c760 100644 --- a/commit_panel.go +++ b/commit_panel.go @@ -18,7 +18,6 @@ func handleCommitPress(g *gocui.Gui, currentView *gocui.View) error { return nil } maxX, maxY := g.Size() - // var v *gocui.View if v, err := g.SetView("commit", maxX/2-30, maxY/2-1, maxX/2+30, maxY/2+1); err != nil { if err != gocui.ErrUnknownView { return err @@ -51,7 +50,9 @@ func handleCommitSubmit(g *gocui.Gui, v *gocui.View) error { func closeCommitPrompt(g *gocui.Gui, v *gocui.View) error { filesView, _ := g.View("files") - switchFocus(g, v, filesView) + // not passing in the view as oldView to switchFocus because we don't want a + // reference pointing to a deleted view + switchFocus(g, nil, filesView) devLog("test prompt close") if err := g.DeleteView("commit"); err != nil { return err diff --git a/view_helpers.go b/view_helpers.go index 01af1cdeb..fe643c5a7 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -24,9 +24,8 @@ func returnFocus(g *gocui.Gui, v *gocui.View) error { func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { if oldView != nil { oldView.Highlight = false - devLog("setting previous view to ") + devLog("setting previous view to:", oldView.Name()) state.PreviousView = oldView.Name() - devLog(state.PreviousView) } newView.Highlight = true devLog(newView.Name()) From 787e5d13edf027043a902b27cf46e6843b1c7013 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 26 May 2018 17:18:00 +1000 Subject: [PATCH 23/32] Showing modal when no files to commit --- commit_panel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commit_panel.go b/commit_panel.go index 5de77c760..f53f93408 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 nil + return createConfirmationPanel(g, currentView, "Nothing to Commit", "There are no staged files to commit (enter)", nil, nil) } maxX, maxY := g.Size() if v, err := g.SetView("commit", maxX/2-30, maxY/2-1, maxX/2+30, maxY/2+1); err != nil { From ec78c795dd8970c0812626d926d3eca5c29e109f Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 27 May 2018 16:32:09 +1000 Subject: [PATCH 24/32] More stuff --- branches_panel.go | 27 ++++++++---- commit_panel.go | 4 +- commits_panel.go | 79 ++++++++++++++++++++++++++++++++++ confirmation_panel.go | 85 ++++++++++++++++++++++++++++++------- files_panel.go | 33 ++++++++++++--- gitcommands.go | 98 ++++++++++++++++++++++++++++++++++--------- gui.go | 50 +++++++++++++++++----- logs_panel.go | 30 ------------- main.go | 4 ++ view_helpers.go | 2 +- 10 files changed, 320 insertions(+), 92 deletions(-) create mode 100644 commits_panel.go delete mode 100644 logs_panel.go 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) } From b6eaa44cc2b1ca0f4088f3590b33125386c5f532 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 1 Jun 2018 23:23:31 +1000 Subject: [PATCH 25/32] more stuff --- branches_panel.go | 31 ++++------ commit_panel.go | 1 - commits_panel.go | 42 +++++++++---- confirmation_panel.go | 5 ++ files_panel.go | 87 ++++++++++++++++++--------- gitcommands.go | 137 +++++++++++++++++++++++++++++------------- gui.go | 67 +++++++++++++++------ main.go | 1 + 8 files changed, 254 insertions(+), 117 deletions(-) diff --git a/branches_panel.go b/branches_panel.go index 576bc01ec..75408f266 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -13,7 +13,8 @@ import ( // "strings" - "github.com/fatih/color" + "fmt" + "github.com/jroimartin/gocui" ) @@ -41,7 +42,11 @@ func getSelectedBranch(v *gocui.View) Branch { } func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { - renderString(g, "options", "space: checkout, s: squash down") + renderString(g, "options", "space: checkout, f: force checkout") + if len(state.Branches) == 0 { + return renderString(g, "main", "No branches for this repo") + } + // may want to standardise how these select methods work lineNumber := getItemPosition(v) branch := state.Branches[lineNumber] diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) @@ -51,33 +56,19 @@ func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { return nil } +// refreshStatus is called at the end of this because that's when we can +// be sure there is a state.Branches array to pick the current branch from 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) - white := color.New(color.FgWhite) - green := color.New(color.FgGreen) - v.Clear() for _, branch := range state.Branches { - if branch.Type == "feature" { - green.Fprintln(v, branch.DisplayString) - continue - } - if branch.Type == "bugfix" { - yellow.Fprintln(v, branch.DisplayString) - continue - } - if branch.Type == "hotfix" { - red.Fprintln(v, branch.DisplayString) - continue - } - white.Fprintln(v, branch.DisplayString) + fmt.Fprintln(v, branch.DisplayString) } resetOrigin(v) + refreshStatus(g) return nil } diff --git a/commit_panel.go b/commit_panel.go index 79a09f17b..b31bcb25a 100644 --- a/commit_panel.go +++ b/commit_panel.go @@ -53,7 +53,6 @@ func closeCommitPrompt(g *gocui.Gui, v *gocui.View) error { // not passing in the view as oldView to switchFocus because we don't want a // reference pointing to a deleted view switchFocus(g, nil, filesView) - devLog("test prompt close") if err := g.DeleteView("commit"); err != nil { return err } diff --git a/commits_panel.go b/commits_panel.go index 2d355a625..f1ca7fc99 100644 --- a/commits_panel.go +++ b/commits_panel.go @@ -7,10 +7,17 @@ package main import ( + "errors" + "github.com/fatih/color" "github.com/jroimartin/gocui" ) +var ( + // ErrNoCommits : When no commits are found for the branch + ErrNoCommits = errors.New("No commits for this branch") +) + func refreshCommits(g *gocui.Gui) error { state.Commits = getCommits() g.Update(func(*gocui.Gui) error { @@ -19,10 +26,17 @@ func refreshCommits(g *gocui.Gui) error { panic(err) } v.Clear() + red := color.New(color.FgRed) yellow := color.New(color.FgYellow) white := color.New(color.FgWhite) + shaColor := white for _, commit := range state.Commits { - yellow.Fprint(v, commit.Sha+" ") + if commit.Pushed { + shaColor = red + } else { + shaColor = yellow + } + shaColor.Fprint(v, commit.Sha+" ") white.Fprintln(v, commit.Name) } return nil @@ -31,9 +45,15 @@ func refreshCommits(g *gocui.Gui) error { } func handleCommitSelect(g *gocui.Gui, v *gocui.View) error { - commit := getSelectedCommit(v) + renderString(g, "options", "s: squash down, r: rename") + commit, err := getSelectedCommit(v) + if err != nil { + if err != ErrNoCommits { + return err + } + return renderString(g, "main", "No commits for this branch") + } commitText := gitShow(commit.Sha) - devLog("commitText:", commitText) return renderString(g, "main", commitText) } @@ -41,7 +61,10 @@ 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) + commit, err := getSelectedCommit(v) + if err != nil { + return err + } if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil { return createSimpleConfirmationPanel(g, v, "Error", output) } @@ -67,13 +90,10 @@ func handleRenameCommit(g *gocui.Gui, v *gocui.View) error { return nil } -func getSelectedCommit(v *gocui.View) Commit { - lineNumber := getItemPosition(v) +func getSelectedCommit(v *gocui.View) (Commit, error) { if len(state.Commits) == 0 { - return Commit{ - Sha: "noCommit", - DisplayString: "none", - } + return Commit{}, ErrNoCommits } - return state.Commits[lineNumber] + lineNumber := getItemPosition(v) + return state.Commits[lineNumber], nil } diff --git a/confirmation_panel.go b/confirmation_panel.go index 08c7e04ab..3db9d4f75 100644 --- a/confirmation_panel.go +++ b/confirmation_panel.go @@ -115,3 +115,8 @@ func createConfirmationPanel(g *gocui.Gui, v *gocui.View, title, prompt string, func createSimpleConfirmationPanel(g *gocui.Gui, v *gocui.View, title, prompt string) error { return createConfirmationPanel(g, v, title, prompt, nil, nil) } + +func createErrorPanel(g *gocui.Gui, message string) error { + v := g.CurrentView() + return createConfirmationPanel(g, v, "Error", message, nil, nil) +} diff --git a/files_panel.go b/files_panel.go index 7e55dfbc3..7d64106e6 100644 --- a/files_panel.go +++ b/files_panel.go @@ -13,12 +13,18 @@ import ( // "strings" + "errors" "strings" "github.com/fatih/color" "github.com/jroimartin/gocui" ) +var ( + // ErrNoFiles : when there are no modified files in the repo + ErrNoFiles = errors.New("No changed files") +) + func stagedFiles(files []GitFile) []GitFile { result := make([]GitFile, 0) for _, file := range files { @@ -30,7 +36,10 @@ func stagedFiles(files []GitFile) []GitFile { } func handleFilePress(g *gocui.Gui, v *gocui.View) error { - file := getSelectedFile(v) + file, err := getSelectedFile(v) + if err != nil { + return err + } if file.HasUnstagedChanges { stageFile(file.Name) @@ -48,24 +57,19 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error { return nil } -func getSelectedFile(v *gocui.View) GitFile { - lineNumber := getItemPosition(v) +func getSelectedFile(v *gocui.View) (GitFile, error) { if len(state.GitFiles) == 0 { - // find a way to not have to do this - return GitFile{ - Name: "noFile", - DisplayString: "none", - HasStagedChanges: false, - HasUnstagedChanges: false, - Tracked: false, - Deleted: false, - } + return GitFile{}, ErrNoFiles } - return state.GitFiles[lineNumber] + lineNumber := getItemPosition(v) + return state.GitFiles[lineNumber], nil } func handleFileRemove(g *gocui.Gui, v *gocui.View) error { - file := getSelectedFile(v) + file, err := getSelectedFile(v) + if err != nil { + return err + } var deleteVerb string if file.Tracked { deleteVerb = "checkout" @@ -80,30 +84,54 @@ func handleFileRemove(g *gocui.Gui, v *gocui.View) error { }, nil) } +func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { + file, err := getSelectedFile(v) + if err != nil { + return err + } + if file.Tracked { + return createErrorPanel(g, "Cannot ignore tracked files") + } + gitIgnore(file.Name) + return refreshFiles(g) +} + func handleFileSelect(g *gocui.Gui, v *gocui.View) error { - item := getSelectedFile(v) + baseString := "tab: switch to branches, space: toggle staged, c: commit changes, o: open, s: open in sublime, i: ignore" + item, err := getSelectedFile(v) + if err != nil { + if err != ErrNoFiles { + return err + } + renderString(g, "main", "No changed files") + colorLog(color.FgRed, "error") + return renderString(g, "options", baseString) + } var optionsString string - baseString := "space: toggle staged, c: commit changes, option+o: open" if item.Tracked { - optionsString = baseString + ", option+d: checkout" + optionsString = baseString + ", r: checkout" } else { - optionsString = baseString + ", option+d: delete" + optionsString = baseString + ", r: delete" } renderString(g, "options", optionsString) diff := getDiff(item) return renderString(g, "main", diff) } -func handleFileOpen(g *gocui.Gui, v *gocui.View) error { - file := getSelectedFile(v) - _, err := openFile(file.Name) +func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, error)) error { + file, err := getSelectedFile(v) + if err != nil { + return err + } + _, err = open(file.Name) return err } +func handleFileOpen(g *gocui.Gui, v *gocui.View) error { + return genericFileOpen(g, v, openFile) +} func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { - file := getSelectedFile(v) - _, err := sublimeOpenFile(file.Name) - return err + return genericFileOpen(g, v, sublimeOpenFile) } func refreshFiles(g *gocui.Gui) error { @@ -144,10 +172,13 @@ func pullFiles(g *gocui.Gui, v *gocui.View) error { createSimpleConfirmationPanel(g, v, "Error", output) } else { closeConfirmationPrompt(g) + refreshCommits(g) + refreshFiles(g) + refreshStatus(g) + devLog("pulled.") } }() - devLog("pulled.") - return refreshFiles(g) + return nil } func pushFiles(g *gocui.Gui, v *gocui.View) error { @@ -158,8 +189,10 @@ func pushFiles(g *gocui.Gui, v *gocui.View) error { createSimpleConfirmationPanel(g, v, "Error", output) } else { closeConfirmationPrompt(g) + refreshCommits(g) + refreshStatus(g) + devLog("pushed.") } }() - devLog("pushed.") return nil } diff --git a/gitcommands.go b/gitcommands.go index b78da318b..640349569 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -18,25 +18,27 @@ import ( // GitFile : A staged/unstaged file type GitFile struct { Name string - DisplayString string HasStagedChanges bool HasUnstagedChanges bool Tracked bool Deleted bool + DisplayString string } // Branch : A git branch type Branch struct { Name string - DisplayString string Type string BaseBranch string + DisplayString string + DisplayColor color.Attribute } // Commit : A git commit type Commit struct { Sha string Name string + Pushed bool DisplayString string } @@ -100,43 +102,61 @@ func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { func runDirectCommand(command string) (string, error) { commandLog(command) cmdOut, err := exec.Command("bash", "-c", command).CombinedOutput() - devLog(string(cmdOut)) - devLog(err) return string(cmdOut), err } -func branchNameFromString(branchString string) string { - // because this has the recency at the beginning, - // we need to split and take the second part +func branchStringParts(branchString string) (string, string) { splitBranchName := strings.Split(branchString, "\t") - return splitBranchName[len(splitBranchName)-1] + return splitBranchName[0], splitBranchName[1] +} + +// branchPropertiesFromName : returns branch type, base, and color +func branchPropertiesFromName(name string) (string, string, color.Attribute) { + if strings.Contains(name, "feature/") { + return "feature", "develop", color.FgGreen + } else if strings.Contains(name, "bugfix/") { + return "bugfix", "develop", color.FgYellow + } else if strings.Contains(name, "hotfix/") { + return "hotfix", "master", color.FgRed + } + return "other", name, color.FgWhite +} + +func coloredString(str string, colour color.Attribute) string { + return color.New(colour).SprintFunc()(fmt.Sprint(str)) +} + +func withPadding(str string, padding int) string { + return str + strings.Repeat(" ", padding-len(str)) +} + +func branchFromLine(line string, index int) Branch { + recency, name := branchStringParts(line) + branchType, branchBase, colour := branchPropertiesFromName(name) + if index == 0 { + recency = " *" + } + displayString := withPadding(recency, 4) + coloredString(name, colour) + return Branch{ + Name: name, + Type: branchType, + BaseBranch: branchBase, + DisplayString: displayString, + DisplayColor: colour, + } } func getGitBranches() []Branch { branches := make([]Branch, 0) + // check if there are any branches + branchCheck, _ := runDirectCommand("git branch") + if branchCheck == "" { + return branches + } rawString, _ := runDirectCommand(getBranchesCommand) branchLines := splitLines(rawString) for i, line := range branchLines { - name := branchNameFromString(line) - var branchType string - var baseBranch string - if strings.Contains(line, "feature/") { - branchType = "feature" - baseBranch = "develop" - } else if strings.Contains(line, "bugfix/") { - branchType = "bugfix" - baseBranch = "develop" - } else if strings.Contains(line, "hotfix/") { - branchType = "hotfix" - baseBranch = "master" - } else { - branchType = "other" - baseBranch = name - } - if i == 0 { - line = "* " + line - } - branches = append(branches, Branch{name, line, branchType, baseBranch}) + branches = append(branches, branchFromLine(line, i)) } devLog(branches) return branches @@ -182,7 +202,6 @@ 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 } @@ -198,30 +217,50 @@ func getBranchDiff(branch string, baseBranch string) (string, error) { return runCommand("git diff --color " + baseBranch + "..." + branch) } +func verifyInGitRepo() { + if output, err := runCommand("git status"); err != nil { + fmt.Println(output) + os.Exit(1) + } +} + func getCommits() []Commit { + pushables := gitCommitsToPush() 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, " ")}) + sha := splitLine[0] + pushed := includes(pushables, sha) + commits = append(commits, Commit{ + Sha: sha, + Name: strings.Join(splitLine[1:], " "), + Pushed: pushed, + DisplayString: strings.Join(splitLine, " "), + }) } - devLog(commits) return commits } func getLog() string { result, err := runDirectCommand("git log --oneline") if err != nil { - panic(err) + // assume if there is an error there are no commits yet for this branch + return "" } return result } +func gitIgnore(filename string) { + if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil { + panic(err) + } +} + func gitShow(sha string) string { result, err := runDirectCommand("git show --color " + sha) - // result, err := runDirectCommand("git show --color 10fd353") if err != nil { panic(err) } @@ -296,18 +335,34 @@ func gitRenameCommit(message string) (string, error) { return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"") } -func betterHaveWorked(err error) { +func gitUpstreamDifferenceCount() (string, string) { + // TODO: deal with these errors which appear when we haven't yet pushed a feature branch + pushableCount, err := runDirectCommand("git rev-list @{u}..head --count") if err != nil { - panic(err) + return "?", "?" } + pullableCount, err := runDirectCommand("git rev-list head..@{u} --count") + if err != nil { + return "?", "?" + } + return strings.Trim(pullableCount, " \n"), strings.Trim(pushableCount, " \n") } -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 +func gitCommitsToPush() []string { + pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit") + if err != nil { + return make([]string, 0) + } + return splitLines(pushables) +} + +func gitCurrentBranchName() string { + branchName, err := runDirectCommand("git rev-parse --abbrev-ref HEAD") + // if there is an error, assume there are no branches yet + if err != nil { + return "" + } + return branchName } const getBranchesCommand = `set -e diff --git a/gui.go b/gui.go index 442433981..d99db6baa 100644 --- a/gui.go +++ b/gui.go @@ -11,6 +11,7 @@ import ( // "io" // "io/ioutil" + "fmt" "log" // "strings" @@ -140,10 +141,13 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { return err } - if err := g.SetKeybinding("files", 'p', gocui.ModNone, pullFiles); err != nil { + if err := g.SetKeybinding("", 'P', gocui.ModNone, pushFiles); err != nil { return err } - if err := g.SetKeybinding("files", 'P', gocui.ModNone, pushFiles); err != nil { + if err := g.SetKeybinding("", 'p', gocui.ModNone, pullFiles); err != nil { + return err + } + 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 { @@ -177,19 +181,39 @@ func handleLogState(g *gocui.Gui, v *gocui.View) error { return nil } +func refreshStatus(g *gocui.Gui) error { + v, err := g.View("status") + if err != nil { + return err + } + up, down := gitUpstreamDifferenceCount() + devLog(up, down) + fmt.Fprint(v, "↑"+up+"↓"+down) + branches := state.Branches + if len(branches) == 0 { + return nil + } + branch := branches[0] + // utilising the fact these all have padding to only grab the name + // from the display string with the existing coloring applied + fmt.Fprint(v, " "+branch.DisplayString[4:]) + return nil +} + func layout(g *gocui.Gui) error { width, height := g.Size() leftSideWidth := width / 3 logsBranchesBoundary := height - 10 filesBranchesBoundary := height - 20 + statusFilesBoundary := 2 - optionsTop := height - 3 + optionsTop := height - 2 // hiding options if there's not enough space if height < 30 { - optionsTop = height + optionsTop = height - 1 } - sideView, err := g.SetView("files", 0, 0, leftSideWidth, filesBranchesBoundary-1) + sideView, err := g.SetView("files", 0, statusFilesBoundary+1, leftSideWidth, filesBranchesBoundary-1) if err != nil { if err != gocui.ErrUnknownView { return err @@ -199,7 +223,14 @@ func layout(g *gocui.Gui) error { refreshFiles(g) } - if v, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop-1); err != nil { + if v, err := g.SetView("status", 0, statusFilesBoundary-2, leftSideWidth, statusFilesBoundary); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Status" + } + + if v, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop); err != nil { if err != gocui.ErrUnknownView { return err } @@ -209,16 +240,6 @@ func layout(g *gocui.Gui) error { handleFileSelect(g, sideView) } - if v, err := g.SetView("commits", 0, logsBranchesBoundary, leftSideWidth, optionsTop-1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Commits" - - // these are only called once - refreshCommits(g) - } - if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, logsBranchesBoundary-1); err != nil { if err != gocui.ErrUnknownView { return err @@ -230,10 +251,22 @@ func layout(g *gocui.Gui) error { nextView(g, nil) } - if v, err := g.SetView("options", 0, optionsTop, width-1, optionsTop+2); err != nil { + if v, err := g.SetView("commits", 0, logsBranchesBoundary, leftSideWidth, optionsTop); err != nil { if err != gocui.ErrUnknownView { return err } + v.Title = "Commits" + + // these are only called once + refreshCommits(g) + } + + if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.BgColor = gocui.ColorBlue + v.Frame = false v.Title = "Options" } diff --git a/main.go b/main.go index 36d35ff68..10272d56b 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import "github.com/fatih/color" func main() { + verifyInGitRepo() a, b := gitUpstreamDifferenceCount() colorLog(color.FgRed, a, b) devLog("\n\n\n\n\n\n\n\n\n\n") From a555a75565c2737a41583063936a70ab5e07c0e4 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 1 Jun 2018 23:25:15 +1000 Subject: [PATCH 26/32] clearing status before re-writing contents --- gui.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gui.go b/gui.go index d99db6baa..bb07bb789 100644 --- a/gui.go +++ b/gui.go @@ -186,6 +186,7 @@ func refreshStatus(g *gocui.Gui) error { if err != nil { return err } + v.Clear() up, down := gitUpstreamDifferenceCount() devLog(up, down) fmt.Fprint(v, "↑"+up+"↓"+down) From 103a6fd21970db5590040e3eef28a2228e7279ef Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 2 Jun 2018 08:35:49 +1000 Subject: [PATCH 27/32] logging durations and more stuff --- .gitignore | 1 + branches_panel.go | 50 +++++++++++++++++++---------------------------- commit_panel.go | 8 -------- commits_panel.go | 6 ------ files_panel.go | 6 ------ gitcommands.go | 23 +++++++++++----------- gui.go | 27 ------------------------- main.go | 12 ++++++++---- status_panel.go | 30 ++++++++++++++++++++++++++++ view_helpers.go | 3 +++ 10 files changed, 73 insertions(+), 93 deletions(-) create mode 100644 status_panel.go diff --git a/.gitignore b/.gitignore index 48dffc2a7..5583d74d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ development.log commands.log +extra/lgit.rb diff --git a/branches_panel.go b/branches_panel.go index 75408f266..3ff79afda 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -1,18 +1,6 @@ -// 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 ( - - // "io" - // "io/ioutil" - - // "strings" - "fmt" "github.com/jroimartin/gocui" @@ -41,34 +29,36 @@ func getSelectedBranch(v *gocui.View) Branch { return state.Branches[lineNumber] } +// may want to standardise how these select methods work func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { renderString(g, "options", "space: checkout, f: force checkout") if len(state.Branches) == 0 { return renderString(g, "main", "No branches for this repo") } - // may want to standardise how these select methods work - lineNumber := getItemPosition(v) - branch := state.Branches[lineNumber] - diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) - if err := renderString(g, "main", diff); err != nil { - return err - } + go func() { + lineNumber := getItemPosition(v) + branch := state.Branches[lineNumber] + diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) + renderString(g, "main", diff) + }() return nil } // refreshStatus is called at the end of this because that's when we can // be sure there is a state.Branches array to pick the current branch from func refreshBranches(g *gocui.Gui) error { - v, err := g.View("branches") - if err != nil { - panic(err) - } - state.Branches = getGitBranches() - v.Clear() - for _, branch := range state.Branches { - fmt.Fprintln(v, branch.DisplayString) - } - resetOrigin(v) - refreshStatus(g) + g.Update(func(g *gocui.Gui) error { + v, err := g.View("branches") + if err != nil { + panic(err) + } + state.Branches = getGitBranches() + v.Clear() + for _, branch := range state.Branches { + fmt.Fprintln(v, branch.DisplayString) + } + resetOrigin(v) + return refreshStatus(g) + }) return nil } diff --git a/commit_panel.go b/commit_panel.go index b31bcb25a..a3d5d9b68 100644 --- a/commit_panel.go +++ b/commit_panel.go @@ -1,9 +1,3 @@ -// 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 ( @@ -13,7 +7,6 @@ import ( ) func handleCommitPress(g *gocui.Gui, currentView *gocui.View) error { - devLog(stagedFiles(state.GitFiles)) if len(stagedFiles(state.GitFiles)) == 0 { return createSimpleConfirmationPanel(g, currentView, "Nothing to Commit", "There are no staged files to commit (esc)") } @@ -40,7 +33,6 @@ func handleCommitSubmit(g *gocui.Gui, v *gocui.View) error { // for whatever reason, a successful commit returns an error, so we're not // going to check for an error here if err := gitCommit(message); err != nil { - devLog(err) panic(err) } refreshFiles(g) diff --git a/commits_panel.go b/commits_panel.go index f1ca7fc99..ec7681b3a 100644 --- a/commits_panel.go +++ b/commits_panel.go @@ -1,9 +1,3 @@ -// 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 ( diff --git a/files_panel.go b/files_panel.go index 7d64106e6..66d0acb34 100644 --- a/files_panel.go +++ b/files_panel.go @@ -1,9 +1,3 @@ -// 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 ( diff --git a/gitcommands.go b/gitcommands.go index 640349569..3cadaa08f 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -1,7 +1,3 @@ -// Go has various value types including strings, -// integers, floats, booleans, etc. Here are a few -// basic examples. - package main import ( @@ -11,6 +7,7 @@ import ( "os" "os/exec" "strings" + "time" "github.com/fatih/color" ) @@ -52,7 +49,7 @@ func colorLog(colour color.Attribute, objects ...interface{}) { func commandLog(objects ...interface{}) { 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...) + // localLog(color.FgWhite, "/Users/jesseduffieldduffield/go/src/github.com/jesseduffield/gitgot/development.log", objects...) } func localLog(colour color.Attribute, path string, objects ...interface{}) { @@ -100,8 +97,12 @@ func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { } func runDirectCommand(command string) (string, error) { + timeStart := time.Now() + commandLog(command) cmdOut, err := exec.Command("bash", "-c", command).CombinedOutput() + devLog("run direct command time for command: ", command, time.Now().Sub(timeStart)) + return string(cmdOut), err } @@ -158,18 +159,12 @@ func getGitBranches() []Branch { for i, line := range branchLines { branches = append(branches, branchFromLine(line, i)) } - devLog(branches) return branches } func getGitStatusFiles() []GitFile { statusOutput, _ := getGitStatus() statusStrings := splitLines(statusOutput) - devLog(statusStrings) - // a file can have both staged and unstaged changes - // I'll probably end up ignoring the unstaged flag for now but might revisit - // tracked, staged, unstaged - gitFiles := make([]GitFile, 0) for _, statusString := range statusStrings { @@ -199,9 +194,11 @@ func gitCheckout(branch string, force bool) (string, error) { } func runCommand(command string) (string, error) { + startTime := time.Now() commandLog(command) splitCmd := strings.Split(command, " ") cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() + devLog("run command time: ", time.Now().Sub(startTime)) return string(cmdOut), err } @@ -245,7 +242,9 @@ func getCommits() []Commit { } func getLog() string { - result, err := runDirectCommand("git log --oneline") + // currently limiting to 30 for performance reasons + // TODO: add lazyloading when you scroll down + result, err := runDirectCommand("git log --oneline -30") if err != nil { // assume if there is an error there are no commits yet for this branch return "" diff --git a/gui.go b/gui.go index bb07bb789..c16fc1cd5 100644 --- a/gui.go +++ b/gui.go @@ -1,9 +1,3 @@ -// 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 ( @@ -11,7 +5,6 @@ import ( // "io" // "io/ioutil" - "fmt" "log" // "strings" @@ -181,26 +174,6 @@ func handleLogState(g *gocui.Gui, v *gocui.View) error { return nil } -func refreshStatus(g *gocui.Gui) error { - v, err := g.View("status") - if err != nil { - return err - } - v.Clear() - up, down := gitUpstreamDifferenceCount() - devLog(up, down) - fmt.Fprint(v, "↑"+up+"↓"+down) - branches := state.Branches - if len(branches) == 0 { - return nil - } - branch := branches[0] - // utilising the fact these all have padding to only grab the name - // from the display string with the existing coloring applied - fmt.Fprint(v, " "+branch.DisplayString[4:]) - return nil -} - func layout(g *gocui.Gui) error { width, height := g.Size() leftSideWidth := width / 3 diff --git a/main.go b/main.go index 10272d56b..66f9686b3 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,15 @@ package main -import "github.com/fatih/color" +import ( + "time" +) + +// StartTime : The starting time of the app +var StartTime time.Time func main() { - verifyInGitRepo() - a, b := gitUpstreamDifferenceCount() - colorLog(color.FgRed, a, b) devLog("\n\n\n\n\n\n\n\n\n\n") + StartTime = time.Now() + verifyInGitRepo() run() } diff --git a/status_panel.go b/status_panel.go new file mode 100644 index 000000000..5172f19f8 --- /dev/null +++ b/status_panel.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "time" + + "github.com/fatih/color" + "github.com/jroimartin/gocui" +) + +func refreshStatus(g *gocui.Gui) error { + v, err := g.View("status") + if err != nil { + return err + } + v.Clear() + up, down := gitUpstreamDifferenceCount() + fmt.Fprint(v, "↑"+up+"↓"+down) + branches := state.Branches + if len(branches) == 0 { + return nil + } + branch := branches[0] + // utilising the fact these all have padding to only grab the name + // from the display string with the existing coloring applied + fmt.Fprint(v, " "+branch.DisplayString[4:]) + + colorLog(color.FgCyan, time.Now().Sub(StartTime)) + return nil +} diff --git a/view_helpers.go b/view_helpers.go index b8acb524b..912063ca5 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -9,6 +9,7 @@ package main import ( "fmt" "strings" + "time" "github.com/jroimartin/gocui" ) @@ -97,6 +98,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() v, err := g.View(viewName) if err != nil { panic(err) @@ -104,6 +106,7 @@ func renderString(g *gocui.Gui, viewName, s string) error { v.Clear() fmt.Fprint(v, s) v.Wrap = true + devLog("render time: ", time.Now().Sub(timeStart)) return nil }) return nil From 157278e06ee6d14412be4d18d13cd4470f2aa089 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 2 Jun 2018 08:45:30 +1000 Subject: [PATCH 28/32] stop tracking notes --- notes/go.notes | 78 -------------------------------------------------- 1 file changed, 78 deletions(-) delete mode 100644 notes/go.notes diff --git a/notes/go.notes b/notes/go.notes deleted file mode 100644 index 1e0c0b8d0..000000000 --- a/notes/go.notes +++ /dev/null @@ -1,78 +0,0 @@ -TODO: - -committing -blowing away files: - if it's untracked, delete it - if it's tracked, check it out - - - ------------------------------------------------------------ - GO ------------------------------------------------------------ - -Running and Building: - -$ go run hello-world.go -hello world - -$ go build hello-world.go -$ ls -hello-world hello-world.go - -$ ./hello-world -hello world - ------------------------------------------------------------ - DIRECTORY STRUCTURE ------------------------------------------------------------ - -https://golang.org/doc/code.html - -if you don't have your GOPATH exported, do so with -export GOPATH=$(go env GOPATH) - -you have a GOPATH which points to e.g. ~/go/ -this is where everything in go is stored. - -it has three directories, - - src - - pkg - - bin - -installed programs have their executables stored in bin -all your project and the src code of other people's projects are in src, with paths identifying them e.g. -src/github.com/jesseduffield/gitgot - -If you want to make an executable, give every file in your project directory `package main`, otherwise if you want to make a package, name everything `package mypackage`. This name should be the name of the project directory for your project. - -to install a program, inside the project folder -go install - -This adds the program to /bin - -to build a package, inside the project folder -go build - -this adds the package to /pkg so that it can be linked easily in the future. - -to build and run your program just do this: - -~/github.com/jesseduffield/gitgot: -▶ go install && gitgot - ------------------------------------------------------------ - BUILD SYSTEMS ------------------------------------------------------------ - -Currently for installs I'm using -/Users/jesseduffieldduffield/Library/Application Support/Sublime Text 3/Packages/User/custom_go_install.sublime-build - -note that the argument to go install should be the directory from the end of /src/ onwards. - ------------------------------------------------------------ - FMT ------------------------------------------------------------ - -casting anything to a string -fmt.Sprint(thing) From 5ccea4f5d9a709dc19fac4176d411d92bdfc8441 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 2 Jun 2018 09:03:39 +1000 Subject: [PATCH 29/32] ignore notes --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5583d74d0..bb150c3dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ development.log commands.log extra/lgit.rb +notes/go.notes From 7cdcef8c931ea77dab3345b5c936c26b19a69c2d Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 2 Jun 2018 09:05:20 +1000 Subject: [PATCH 30/32] picking up new files upon file refresh --- gitcommands.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/gitcommands.go b/gitcommands.go index 3cadaa08f..6ebfabcfc 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -70,7 +70,18 @@ func Map(vs []string, f func(string) string) []string { return vsm } -func includes(list []string, a string) bool { +func includesString(list []string, a string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +// not sure how to genericise this because []interface{} doesn't accept e.g. +// []int arguments +func includesInt(list []int, a int) bool { for _, b := range list { if b == a { return true @@ -84,15 +95,27 @@ func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { return newGitFiles } + appendedIndexes := make([]int, 0) + + // retain position of files we already could see result := make([]GitFile, 0) for _, oldGitFile := range oldGitFiles { - for _, newGitFile := range newGitFiles { + for newIndex, newGitFile := range newGitFiles { if oldGitFile.Name == newGitFile.Name { result = append(result, newGitFile) + appendedIndexes = append(appendedIndexes, newIndex) break } } } + + // append any new files to the end + for index, newGitFile := range newGitFiles { + if !includesInt(appendedIndexes, index) { + result = append(result, newGitFile) + } + } + return result } @@ -230,7 +253,7 @@ func getCommits() []Commit { for _, line := range lines { splitLine := strings.Split(line, " ") sha := splitLine[0] - pushed := includes(pushables, sha) + pushed := includesString(pushables, sha) commits = append(commits, Commit{ Sha: sha, Name: strings.Join(splitLine[1:], " "), From a0c8fc8899bc107f58d2081da0754028d1ff5383 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 2 Jun 2018 09:06:02 +1000 Subject: [PATCH 31/32] loading file diff after refreshing files incase cursor is now at a different file --- files_panel.go | 1 + 1 file changed, 1 insertion(+) diff --git a/files_panel.go b/files_panel.go index 66d0acb34..40beda2f0 100644 --- a/files_panel.go +++ b/files_panel.go @@ -155,6 +155,7 @@ func refreshFiles(g *gocui.Gui) error { } } correctCursor(filesView) + handleFileSelect(g, filesView) return nil } From 80bcc7c16eb0a9c25d408785213e3b1cad48915b Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 2 Jun 2018 13:51:03 +1000 Subject: [PATCH 32/32] More stuff --- branches_panel.go | 13 +++++++++++++ commits_panel.go | 44 +++++++++++++++++++++++++++++++++++++++----- gitcommands.go | 15 +++++++++++++-- gui.go | 27 ++++++++++++++++++++++----- status_panel.go | 30 ++++++++++++++++++------------ view_helpers.go | 6 ------ 6 files changed, 105 insertions(+), 30 deletions(-) diff --git a/branches_panel.go b/branches_panel.go index 3ff79afda..96071efd3 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -24,6 +24,19 @@ func handleForceCheckout(g *gocui.Gui, v *gocui.View) error { }, nil) } +func handleNewBranch(g *gocui.Gui, v *gocui.View) error { + branch := state.Branches[0] + createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error { + // TODO: make sure the buffer is stripped of whitespace + if output, err := gitNewBranch(v.Buffer()); err != nil { + return createSimpleConfirmationPanel(g, v, "Error", output) + } + refreshSidePanels(g, v) + return handleCommitSelect(g, v) + }) + return nil +} + func getSelectedBranch(v *gocui.View) Branch { lineNumber := getItemPosition(v) return state.Branches[lineNumber] diff --git a/commits_panel.go b/commits_panel.go index ec7681b3a..1df9e2ad9 100644 --- a/commits_panel.go +++ b/commits_panel.go @@ -13,8 +13,8 @@ var ( ) func refreshCommits(g *gocui.Gui) error { - state.Commits = getCommits() g.Update(func(*gocui.Gui) error { + state.Commits = getCommits() v, err := g.View("commits") if err != nil { panic(err) @@ -33,14 +33,36 @@ func refreshCommits(g *gocui.Gui) error { shaColor.Fprint(v, commit.Sha+" ") white.Fprintln(v, commit.Name) } + refreshStatus(g) return nil }) return nil } +func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error { + return createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit? (y/n)", func(g *gocui.Gui, v *gocui.View) error { + commit, err := getSelectedCommit(g) + devLog(commit) + if err != nil { + panic(err) + } + if output, err := gitResetToCommit(commit.Sha); err != nil { + return createSimpleConfirmationPanel(g, commitView, "Error", output) + } + if err := refreshCommits(g); err != nil { + panic(err) + } + if err := refreshFiles(g); err != nil { + panic(err) + } + resetOrigin(commitView) + return handleCommitSelect(g, nil) + }, nil) +} + func handleCommitSelect(g *gocui.Gui, v *gocui.View) error { - renderString(g, "options", "s: squash down, r: rename") - commit, err := getSelectedCommit(v) + renderString(g, "options", "s: squash down, r: rename, g: reset to this commit") + commit, err := getSelectedCommit(g) if err != nil { if err != ErrNoCommits { return err @@ -55,7 +77,10 @@ func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { if getItemPosition(v) != 0 { return createSimpleConfirmationPanel(g, v, "Error", "Can only squash topmost commit") } - commit, err := getSelectedCommit(v) + if len(state.Commits) == 1 { + return createSimpleConfirmationPanel(g, v, "Error", "You have no commits to squash with") + } + commit, err := getSelectedCommit(g) if err != nil { return err } @@ -65,6 +90,7 @@ func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { if err := refreshCommits(g); err != nil { panic(err) } + refreshStatus(g) return handleCommitSelect(g, v) } @@ -84,10 +110,18 @@ func handleRenameCommit(g *gocui.Gui, v *gocui.View) error { return nil } -func getSelectedCommit(v *gocui.View) (Commit, error) { +func getSelectedCommit(g *gocui.Gui) (Commit, error) { + v, err := g.View("commits") + if err != nil { + panic(err) + } if len(state.Commits) == 0 { return Commit{}, ErrNoCommits } lineNumber := getItemPosition(v) + if lineNumber > len(state.Commits)-1 { + colorLog(color.FgRed, "potential error in getSelected Commit (mismatched ui and state)", state.Commits, lineNumber) + return state.Commits[len(state.Commits)-1], nil + } return state.Commits[lineNumber], nil } diff --git a/gitcommands.go b/gitcommands.go index 6ebfabcfc..0a7e9c5a5 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -357,8 +357,19 @@ func gitRenameCommit(message string) (string, error) { return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"") } +func gitFetch() (string, error) { + return runDirectCommand("git fetch") +} + +func gitResetToCommit(sha string) (string, error) { + return runDirectCommand("git reset " + sha) +} + +func gitNewBranch(name string) (string, error) { + return runDirectCommand("git checkout -b " + name) +} + func gitUpstreamDifferenceCount() (string, string) { - // TODO: deal with these errors which appear when we haven't yet pushed a feature branch pushableCount, err := runDirectCommand("git rev-list @{u}..head --count") if err != nil { return "?", "?" @@ -367,7 +378,7 @@ func gitUpstreamDifferenceCount() (string, string) { if err != nil { return "?", "?" } - return strings.Trim(pullableCount, " \n"), strings.Trim(pushableCount, " \n") + return strings.Trim(pushableCount, " \n"), strings.Trim(pullableCount, " \n") } func gitCommitsToPush() []string { diff --git a/gui.go b/gui.go index c16fc1cd5..136334656 100644 --- a/gui.go +++ b/gui.go @@ -6,6 +6,7 @@ import ( // "io/ioutil" "log" + "time" // "strings" "github.com/jroimartin/gocui" @@ -155,22 +156,26 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("branches", 'F', gocui.ModNone, handleForceCheckout); err != nil { return err } + if err := g.SetKeybinding("branches", 'n', gocui.ModNone, handleNewBranch); 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 { + if err := g.SetKeybinding("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil { + return err + } + if err := g.SetKeybinding("", 'S', gocui.ModNone, genericTest); err != nil { return err } return nil } -func handleLogState(g *gocui.Gui, v *gocui.View) error { - devLog("state is:", state) - devLog("previous view:", state.PreviousView) - refreshBranches(g) +func genericTest(g *gocui.Gui, v *gocui.View) error { + pushFiles(g, v) return nil } @@ -247,6 +252,11 @@ func layout(g *gocui.Gui) error { return nil } +func fetch(g *gocui.Gui) { + gitFetch() + refreshStatus(g) +} + func run() { g, err := gocui.NewGui(gocui.OutputNormal) if err != nil { @@ -254,6 +264,13 @@ func run() { } defer g.Close() + // periodically fetching to check for upstream differences + go func() { + for range time.Tick(time.Second * 60) { + fetch(g) + } + }() + g.SetManagerFunc(layout) if err := keybindings(g); err != nil { diff --git a/status_panel.go b/status_panel.go index 5172f19f8..1a36c486b 100644 --- a/status_panel.go +++ b/status_panel.go @@ -11,20 +11,26 @@ import ( func refreshStatus(g *gocui.Gui) error { v, err := g.View("status") if err != nil { - return err + panic(err) } - v.Clear() - up, down := gitUpstreamDifferenceCount() - fmt.Fprint(v, "↑"+up+"↓"+down) - branches := state.Branches - if len(branches) == 0 { + // for some reason if this isn't wrapped in an update the clear seems to + // be applied after the other things or something like that; the panel's + // contents end up cleared + g.Update(func(*gocui.Gui) error { + v.Clear() + pushables, pullables := gitUpstreamDifferenceCount() + fmt.Fprint(v, "↑"+pushables+"↓"+pullables) + branches := state.Branches + if len(branches) == 0 { + return nil + } + branch := branches[0] + // utilising the fact these all have padding to only grab the name + // from the display string with the existing coloring applied + fmt.Fprint(v, " "+branch.DisplayString[4:]) + colorLog(color.FgCyan, time.Now().Sub(StartTime)) return nil - } - branch := branches[0] - // utilising the fact these all have padding to only grab the name - // from the display string with the existing coloring applied - fmt.Fprint(v, " "+branch.DisplayString[4:]) + }) - colorLog(color.FgCyan, time.Now().Sub(StartTime)) return nil } diff --git a/view_helpers.go b/view_helpers.go index 912063ca5..4aa3dd5dd 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -1,9 +1,3 @@ -// 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 (