1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-03 00:57:52 +02:00

merge master/better-file-opening into master

This commit is contained in:
Jesse Duffield
2018-08-08 20:50:45 +10:00
9 changed files with 273 additions and 27 deletions

9
Gopkg.lock generated
View File

@ -57,6 +57,14 @@
pruneopts = "NUT" pruneopts = "NUT"
revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3" revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3"
[[projects]]
digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541"
name = "github.com/tcnksm/go-gitconfig"
packages = ["."]
pruneopts = "NUT"
revision = "d154598bacbf4501c095a309753c5d4af66caa81"
version = "v0.1.2"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:4d8a79fbc6fa348fc94afa4235947c5196b7900ed71b94aa5fcbc7e273d150e1" digest = "1:4d8a79fbc6fa348fc94afa4235947c5196b7900ed71b94aa5fcbc7e273d150e1"
@ -72,6 +80,7 @@
"github.com/fatih/color", "github.com/fatih/color",
"github.com/golang-collections/collections/stack", "github.com/golang-collections/collections/stack",
"github.com/jesseduffield/gocui", "github.com/jesseduffield/gocui",
"github.com/tcnksm/go-gitconfig",
] ]
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -63,6 +63,21 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error {
return handleFileSelect(g, v) return handleFileSelect(g, v)
} }
func handleAddPatch(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == ErrNoFiles {
return nil
}
return err
}
if !file.HasUnstagedChanges {
return createErrorPanel(g, "File has no unstaged changes to add")
}
gitAddPatch(g, file.Name)
return err
}
func getSelectedFile(g *gocui.Gui) (GitFile, error) { func getSelectedFile(g *gocui.Gui) (GitFile, error) {
if len(state.GitFiles) == 0 { if len(state.GitFiles) == 0 {
return GitFile{}, ErrNoFiles return GitFile{}, ErrNoFiles
@ -163,7 +178,7 @@ func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if message == "" { if message == "" {
return createErrorPanel(g, "You cannot commit without a commit message") return createErrorPanel(g, "You cannot commit without a commit message")
} }
if output, err := gitCommit(message); err != nil { if output, err := gitCommit(g, message); err != nil {
return createErrorPanel(g, output) return createErrorPanel(g, output)
} }
refreshFiles(g) refreshFiles(g)
@ -172,7 +187,7 @@ func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
return nil return nil
} }
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, error)) error { func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error {
file, err := getSelectedFile(g) file, err := getSelectedFile(g)
if err != nil { if err != nil {
if err != ErrNoFiles { if err != ErrNoFiles {
@ -180,18 +195,24 @@ func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, err
} }
return nil return nil
} }
if output, err := open(file.Name); err != nil { if _, err := open(g, file.Name); err != nil {
return createErrorPanel(g, output) return createErrorPanel(g, err.Error())
} }
return nil return nil
} }
func handleFileEdit(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, editFile)
}
func handleFileOpen(g *gocui.Gui, v *gocui.View) error { func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, openFile) return genericFileOpen(g, v, openFile)
} }
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, sublimeOpenFile) return genericFileOpen(g, v, sublimeOpenFile)
} }
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error { func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, vsCodeOpenFile) return genericFileOpen(g, v, vsCodeOpenFile)
} }

View File

@ -13,11 +13,16 @@ import (
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/gocui"
gitconfig "github.com/tcnksm/go-gitconfig"
) )
var ( var (
// ErrNoCheckedOutBranch : When we have no checked out branch // ErrNoCheckedOutBranch : When we have no checked out branch
ErrNoCheckedOutBranch = errors.New("No currently checked out branch") ErrNoCheckedOutBranch = errors.New("No currently checked out branch")
// ErrNoOpenCommand : When we don't know which command to use to open a file
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
) )
// GitFile : A staged/unstaged file // GitFile : A staged/unstaged file
@ -263,23 +268,73 @@ func runCommand(command string) (string, error) {
commandStartTime := time.Now() commandStartTime := time.Now()
commandLog(command) commandLog(command)
splitCmd := strings.Split(command, " ") splitCmd := strings.Split(command, " ")
devLog(splitCmd)
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
devLog("run command time: ", time.Now().Sub(commandStartTime)) devLog("run command time: ", time.Now().Sub(commandStartTime))
return sanitisedCommandOutput(cmdOut, err) return sanitisedCommandOutput(cmdOut, err)
} }
func openFile(filename string) (string, error) { func vsCodeOpenFile(g *gocui.Gui, filename string) (string, error) {
return runCommand("open " + filename)
}
func vsCodeOpenFile(filename string) (string, error) {
return runCommand("code -r " + filename) return runCommand("code -r " + filename)
} }
func sublimeOpenFile(filename string) (string, error) { func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) {
return runCommand("subl " + filename) return runCommand("subl " + filename)
} }
func openFile(g *gocui.Gui, filename string) (string, error) {
cmdName, cmdTrail, err := getOpenCommand()
if err != nil {
return "", err
}
return runCommand(cmdName + " " + filename + cmdTrail)
}
func getOpenCommand() (string, string, error) {
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
trailMap := map[string]string{
"xdg-open": " &>/dev/null &",
"cygstart": "",
"open": "",
}
for name, trail := range trailMap {
if out, _ := runCommand("which " + name); out != "exit status 1" {
return name, trail, nil
}
}
return "", "", ErrNoOpenCommand
}
func gitAddPatch(g *gocui.Gui, filename string) {
runSubProcess(g, "git", "add", "-p", filename)
}
func editFile(g *gocui.Gui, filename string) (string, error) {
editor, _ := gitconfig.Global("core.editor")
if editor == "" {
editor = os.Getenv("VISUAL")
}
if editor == "" {
editor = os.Getenv("EDITOR")
}
if editor == "" {
return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.")
}
runSubProcess(g, editor, filename)
return "", nil
}
func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) {
subprocess = exec.Command(cmdName, commandArgs...)
subprocess.Stdin = os.Stdin
subprocess.Stdout = os.Stdout
subprocess.Stderr = os.Stderr
g.Update(func(g *gocui.Gui) error {
return ErrSubprocess
})
}
func getBranchGraph(branch string, baseBranch string) (string, error) { func getBranchGraph(branch string, baseBranch string) (string, error) {
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch) return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch)
@ -403,7 +458,12 @@ func removeFile(file GitFile) error {
return err return err
} }
func gitCommit(message string) (string, error) { func gitCommit(g *gocui.Gui, message string) (string, error) {
gpgsign, _ := gitconfig.Global("commit.gpgsign")
if gpgsign != "" {
runSubProcess(g, "bash", "-c", "git commit -m \""+message+"\"")
return "", nil
}
return runDirectCommand("git commit -m \"" + message + "\"") return runDirectCommand("git commit -m \"" + message + "\"")
} }

14
gui.go
View File

@ -5,7 +5,6 @@ import (
// "io" // "io"
// "io/ioutil" // "io/ioutil"
"log"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@ -207,10 +206,10 @@ func updateLoader(g *gocui.Gui) {
} }
} }
func run() { func run() (err error) {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
if err != nil { if err != nil {
log.Panicln(err) return
} }
defer g.Close() defer g.Close()
@ -229,13 +228,12 @@ func run() {
g.SetManagerFunc(layout) g.SetManagerFunc(layout)
if err := keybindings(g); err != nil { if err = keybindings(g); err != nil {
log.Panicln(err) return
} }
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { err = g.MainLoop()
log.Panicln(err) return
}
} }
func quit(g *gocui.Gui, v *gocui.View) error { func quit(g *gocui.Gui, v *gocui.View) error {

View File

@ -30,6 +30,7 @@ func keybindings(g *gocui.Gui) error {
Binding{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress}, Binding{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
Binding{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove}, Binding{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
Binding{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge}, Binding{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
Binding{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
Binding{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen}, Binding{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
Binding{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, Binding{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
@ -37,6 +38,7 @@ func keybindings(g *gocui.Gui) error {
Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
Binding{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge}, Binding{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
Binding{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
Binding{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop}, Binding{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
Binding{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, Binding{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},

24
main.go
View File

@ -1,19 +1,24 @@
package main package main
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/exec"
"os/user" "os/user"
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/gocui"
) )
// ErrSubProcess is raised when we are running a subprocess
var ( var (
ErrSubprocess = errors.New("running subprocess")
subprocess *exec.Cmd
startTime time.Time startTime time.Time
debugging bool
// Rev - Git Revision // Rev - Git Revision
Rev string Rev string
@ -22,7 +27,7 @@ var (
Version = "unversioned" Version = "unversioned"
builddate string builddate string
debuggingPointer = flag.Bool("debug", false, "a boolean") debuggingFlag = flag.Bool("debug", false, "a boolean")
versionFlag = flag.Bool("v", false, "Print the current version") versionFlag = flag.Bool("v", false, "Print the current version")
) )
@ -47,7 +52,7 @@ func commandLog(objects ...interface{}) {
} }
func localLog(colour color.Attribute, path string, objects ...interface{}) { func localLog(colour color.Attribute, path string, objects ...interface{}) {
if !debugging { if !*debuggingFlag {
return return
} }
f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
@ -69,7 +74,6 @@ func navigateToRepoRootDirectory() {
func main() { func main() {
startTime = time.Now() startTime = time.Now()
debugging = *debuggingPointer
devLog("\n\n\n\n\n\n\n\n\n\n") devLog("\n\n\n\n\n\n\n\n\n\n")
flag.Parse() flag.Parse()
if *versionFlag { if *versionFlag {
@ -78,5 +82,15 @@ func main() {
} }
verifyInGitRepo() verifyInGitRepo()
navigateToRepoRootDirectory() navigateToRepoRootDirectory()
run() for {
if err := run(); err != nil {
if err == gocui.ErrQuit {
break
} else if err == ErrSubprocess {
subprocess.Run()
} else {
log.Panicln(err)
}
}
}
} }

View File

@ -0,0 +1,7 @@
#!/bin/bash
# For testing subprocesses that require input
# Ask the user for login details
read -p 'Username: ' user
read -sp 'Password: ' pass
echo
echo Hello $user

22
vendor/github.com/tcnksm/go-gitconfig/LICENSE generated vendored Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2014 tcnksm
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

113
vendor/github.com/tcnksm/go-gitconfig/gitconfig.go generated vendored Normal file
View File

@ -0,0 +1,113 @@
// Package gitconfig enables you to use `~/.gitconfig` values in Golang.
//
// For a full guide visit http://github.com/tcnksm/go-gitconfig
//
// package main
//
// import (
// "github.com/tcnksm/go-gitconfig"
// "fmt"
// )
//
// func main() {
// user, err := gitconfig.Global("user.name")
// if err == nil {
// fmt.Println(user)
// }
// }
//
package gitconfig
import (
"bytes"
"fmt"
"io/ioutil"
"os/exec"
"regexp"
"strings"
"syscall"
)
// Entire extracts configuration value from `$HOME/.gitconfig` file ,
// `$GIT_CONFIG`, /etc/gitconfig or include.path files.
func Entire(key string) (string, error) {
return execGitConfig(key)
}
// Global extracts configuration value from `$HOME/.gitconfig` file or `$GIT_CONFIG`.
func Global(key string) (string, error) {
return execGitConfig("--global", key)
}
// Local extracts configuration value from current project repository.
func Local(key string) (string, error) {
return execGitConfig("--local", key)
}
// GithubUser extracts github.user name from `Entire gitconfig`
// This is same as Entire("github.user")
func GithubUser() (string, error) {
return Entire("github.user")
}
// Username extracts git user name from `Entire gitconfig`.
// This is same as Entire("user.name")
func Username() (string, error) {
return Entire("user.name")
}
// Email extracts git user email from `$HOME/.gitconfig` file or `$GIT_CONFIG`.
// This is same as Global("user.email")
func Email() (string, error) {
return Entire("user.email")
}
// OriginURL extract remote origin url from current project repository.
// This is same as Local("remote.origin.url")
func OriginURL() (string, error) {
return Local("remote.origin.url")
}
// Repository extract repository name of current project repository.
func Repository() (string, error) {
url, err := OriginURL()
if err != nil {
return "", err
}
repo := retrieveRepoName(url)
return repo, nil
}
// Github extracts github token from `Entire gitconfig`.
// This is same as Entire("github.token")
func GithubToken() (string, error) {
return Entire("github.token")
}
func execGitConfig(args ...string) (string, error) {
gitArgs := append([]string{"config", "--get", "--null"}, args...)
var stdout bytes.Buffer
cmd := exec.Command("git", gitArgs...)
cmd.Stdout = &stdout
cmd.Stderr = ioutil.Discard
err := cmd.Run()
if exitError, ok := err.(*exec.ExitError); ok {
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
if waitStatus.ExitStatus() == 1 {
return "", fmt.Errorf("the key `%s` is not found", args[len(args)-1])
}
}
return "", err
}
return strings.TrimRight(stdout.String(), "\000"), nil
}
var RepoNameRegexp = regexp.MustCompile(`.+/([^/]+)(\.git)?$`)
func retrieveRepoName(url string) string {
matched := RepoNameRegexp.FindStringSubmatch(url)
return strings.TrimSuffix(matched[1], ".git")
}