From 9067c3be3ebb58d0ad25afa07d43e8941b087ac7 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 6 Aug 2018 23:29:00 +1000 Subject: [PATCH 01/12] handling file edit --- files_panel.go | 10 +++++++-- gitcommands.go | 56 ++++++++++++++++++++++++++++++++++++++++++-------- gui.go | 3 +++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/files_panel.go b/files_panel.go index dd0a3f2be..2794b0178 100644 --- a/files_panel.go +++ b/files_panel.go @@ -172,7 +172,7 @@ func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { 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) if err != nil { if err != ErrNoFiles { @@ -180,16 +180,22 @@ func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, err } return nil } - _, err = open(file.Name) + _, err = open(g, file.Name) return err } +func handleFileEdit(g *gocui.Gui, v *gocui.View) error { + return genericFileOpen(g, v, editFile) +} + func handleFileOpen(g *gocui.Gui, v *gocui.View) error { return genericFileOpen(g, v, openFile) } + func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { return genericFileOpen(g, v, sublimeOpenFile) } + func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error { return genericFileOpen(g, v, vsCodeOpenFile) } diff --git a/gitcommands.go b/gitcommands.go index 976369317..27b567766 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -11,11 +11,12 @@ import ( "time" "github.com/fatih/color" + "github.com/jesseduffield/gocui" ) var ( - // ErrNoCheckedOutBranch : When we have no checked out branch - ErrNoCheckedOutBranch = errors.New("No currently checked out branch") + // ErrNoCheckedOutBranch : When we have no checked out branch + ErrNoCheckedOutBranch = errors.New("No currently checked out branch") ) // GitFile : A staged/unstaged file @@ -308,18 +309,57 @@ func runCommand(command string) (string, error) { return string(cmdOut), err } -func openFile(filename string) (string, error) { - return runCommand("open " + filename) -} - -func vsCodeOpenFile(filename string) (string, error) { +func vsCodeOpenFile(g *gocui.Gui, filename string) (string, error) { return runCommand("code -r " + filename) } -func sublimeOpenFile(filename string) (string, error) { +func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) { return runCommand("subl " + filename) } +func openFile(g *gocui.Gui, filename string) (string, error) { + cmdName, cmdTrail := getOpenCommand() + return runCommand(cmdName + " " + cmdTrail) +} + +func editFile(g *gocui.Gui, filename string) (string, error) { + editor := os.Getenv("VISUAL") + if editor == "" { + editor = os.Getenv("EDITOR") + } + if editor == "" { + editor = "vi" + } + runSubProcess(g, editor, filename) + return "", nil +} + +func getOpenCommand() (string, string) { + //NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX) + trailMap := map[string]string{ + "xdg-open": "&>/dev/null &", + "cygstart": "", + } + for name, trail := range trailMap { + if out, _ := runCommand("which " + name); out != "" { + return name, trail + } + } + return "open", "" +} + +func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) { + // TODO: find a way to wait for the subprocess without having to + // close and reinitialize the gui + // g.Close() // TODO: find a way to make close properly after uncommenting + cmd := exec.Command(cmdName, commandArgs...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + run() // start another Gui +} + func getBranchDiff(branch string, baseBranch string) (string, error) { return runCommand("git log -p -30 --color --no-merges " + branch) diff --git a/gui.go b/gui.go index 1c55c91ba..b2bda8c72 100644 --- a/gui.go +++ b/gui.go @@ -124,6 +124,9 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { return err } + if err := g.SetKeybinding("files", 'e', gocui.ModNone, handleFileEdit); err != nil { + return err + } if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil { return err } From f6a9c727faa06142208d8189441414b6bedb7b98 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 7 Aug 2018 18:05:43 +1000 Subject: [PATCH 02/12] run subprocess cleanly --- gitcommands.go | 17 ++++++++--------- gui.go | 14 ++++++-------- main.go | 22 +++++++++++++++++++--- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/gitcommands.go b/gitcommands.go index 27b567766..862b93d2e 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -349,15 +349,14 @@ func getOpenCommand() (string, string) { } func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) { - // TODO: find a way to wait for the subprocess without having to - // close and reinitialize the gui - // g.Close() // TODO: find a way to make close properly after uncommenting - cmd := exec.Command(cmdName, commandArgs...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Run() - run() // start another Gui + 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 getBranchDiff(branch string, baseBranch string) (string, error) { diff --git a/gui.go b/gui.go index b2bda8c72..a3a92bd06 100644 --- a/gui.go +++ b/gui.go @@ -5,7 +5,6 @@ import ( // "io" // "io/ioutil" - "log" "strings" "time" @@ -321,10 +320,10 @@ func updateLoader(g *gocui.Gui) { } } -func run() { +func run() (err error) { g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) if err != nil { - log.Panicln(err) + return } defer g.Close() @@ -343,13 +342,12 @@ func run() { g.SetManagerFunc(layout) - if err := keybindings(g); err != nil { - log.Panicln(err) + if err = keybindings(g); err != nil { + return } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Panicln(err) - } + err = g.MainLoop() + return } func quit(g *gocui.Gui, v *gocui.View) error { diff --git a/main.go b/main.go index b3586b04e..a0b87d716 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,25 @@ package main import ( + "errors" "flag" "fmt" "log" "os" + "os/exec" "os/user" "time" "github.com/fatih/color" + "github.com/jesseduffield/gocui" ) +// ErrSubProcess is raised when we are running a subprocess var ( - startTime time.Time - debugging bool + startTime time.Time + debugging bool + ErrSubprocess = errors.New("running subprocess") + subprocess *exec.Cmd ) func homeDirectory() string { @@ -65,5 +71,15 @@ func main() { startTime = time.Now() verifyInGitRepo() navigateToRepoRootDirectory() - run() + for { + if err := run(); err != nil { + if err == gocui.ErrQuit { + break + } else if err == ErrSubprocess { + subprocess.Run() + } else { + log.Panicln(err) + } + } + } } From 2d4801c39d79d6f745537d56d884aa530d517f2c Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 7 Aug 2018 18:14:49 +1000 Subject: [PATCH 03/12] merge updated keybinding structure --- gui.go | 130 +-------------------------------------------------------- 1 file changed, 1 insertion(+), 129 deletions(-) diff --git a/gui.go b/gui.go index 34491e1ad..21d32b83b 100644 --- a/gui.go +++ b/gui.go @@ -5,7 +5,6 @@ import ( // "io" // "io/ioutil" - "log" "runtime" "strings" "time" @@ -82,134 +81,6 @@ type Binding struct { } func keybindings(g *gocui.Gui) error { - if err := g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, nextView); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyArrowLeft, gocui.ModNone, previousView); 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, scrollUpMain); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil { - return err - } - if err := g.SetKeybinding("", 'P', gocui.ModNone, pushFiles); err != nil { - return err - } - if err := g.SetKeybinding("", 'p', gocui.ModNone, pullFiles); err != nil { - return err - } - if err := g.SetKeybinding("", 'R', gocui.ModNone, handleRefresh); err != nil { - return err - } - 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", 'd', gocui.ModNone, handleFileRemove); err != nil { - return err - } - if err := g.SetKeybinding("files", 'm', gocui.ModNone, handleSwitchToMerge); err != nil { - return err - } - if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil { - return err - } - if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { - return err - } - if err := g.SetKeybinding("files", 'e', gocui.ModNone, handleFileEdit); err != nil { - return err - } - if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil { - return err - } - if err := g.SetKeybinding("files", 'r', gocui.ModNone, handleRefreshFiles); err != nil { - return err - } - if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil { - return err - } - if err := g.SetKeybinding("files", 'a', gocui.ModNone, handleAbortMerge); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowUp, gocui.ModNone, handleSelectTop); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyEsc, gocui.ModNone, handleEscapeMerge); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowDown, gocui.ModNone, handleSelectBottom); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, handlePickHunk); err != nil { - return err - } - if err := g.SetKeybinding("main", 'b', gocui.ModNone, handlePickBothHunks); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowLeft, gocui.ModNone, handleSelectPrevConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowRight, gocui.ModNone, handleSelectNextConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", 'z', gocui.ModNone, handlePopFileSnapshot); err != nil { - return err - } - if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'c', gocui.ModNone, handleCheckoutByName); err != nil { - return err - } - 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("branches", 'm', gocui.ModNone, handleMerge); 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("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil { - return err - } - if err := g.SetKeybinding("stash", gocui.KeySpace, gocui.ModNone, handleStashApply); err != nil { - return err - } - // TODO: come up with a better keybinding (p/P used for pushing/pulling which - // I'd like to be global. Perhaps all global keybindings should use a modifier - // like command? But then there's gonna be hotkey conflicts with the terminal - if err := g.SetKeybinding("stash", 'k', gocui.ModNone, handleStashPop); err != nil { - return err - } - if err := g.SetKeybinding("stash", 'd', gocui.ModNone, handleStashDrop); err != nil { - return err bindings := []Binding{ Binding{ViewName: "", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView}, Binding{ViewName: "", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView}, @@ -227,6 +98,7 @@ func keybindings(g *gocui.Gui) error { 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: '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: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, From b54198700746a52d2a19f73fa8af8f2977f98253 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 7 Aug 2018 18:37:48 +1000 Subject: [PATCH 04/12] fixup --- files_panel.go | 2 +- gitcommands.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/files_panel.go b/files_panel.go index fc795617f..41c2a4aeb 100644 --- a/files_panel.go +++ b/files_panel.go @@ -180,7 +180,7 @@ func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) } return nil } - if output, err := open(file.Name); err != nil { + if output, err := open(g, file.Name); err != nil { return createErrorPanel(g, output) } return nil diff --git a/gitcommands.go b/gitcommands.go index e8f1bcbc3..5bc991bfa 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -384,7 +384,6 @@ func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) { }) } -func getBranchDiff(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) From 5a624764a8497105829b2483b2b9e253d7110dc5 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 7 Aug 2018 19:50:35 +1000 Subject: [PATCH 05/12] support patched adding --- files_panel.go | 13 +++++++++++++ gitcommands.go | 5 +++++ gui.go | 1 + 3 files changed, 19 insertions(+) diff --git a/files_panel.go b/files_panel.go index 41c2a4aeb..2d7f38e32 100644 --- a/files_panel.go +++ b/files_panel.go @@ -63,6 +63,19 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error { 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 + } + + gitAddPatch(g, file.Name) + return err +} + func getSelectedFile(g *gocui.Gui) (GitFile, error) { if len(state.GitFiles) == 0 { return GitFile{}, ErrNoFiles diff --git a/gitcommands.go b/gitcommands.go index 5bc991bfa..78b45a440 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -347,6 +347,10 @@ func openFile(g *gocui.Gui, filename string) (string, error) { return runCommand(cmdName + " " + cmdTrail) } +func gitAddPatch(g *gocui.Gui, filename string) { + runSubProcess(g, "git", "add", "-p", filename) +} + func editFile(g *gocui.Gui, filename string) (string, error) { editor := os.Getenv("VISUAL") if editor == "" { @@ -355,6 +359,7 @@ func editFile(g *gocui.Gui, filename string) (string, error) { if editor == "" { editor = "vi" } + runSubProcess(g, editor, filename) return "", nil } diff --git a/gui.go b/gui.go index 21d32b83b..ba457bda0 100644 --- a/gui.go +++ b/gui.go @@ -106,6 +106,7 @@ func keybindings(g *gocui.Gui) error { Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, 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.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, From 417cb97dc63b08594d75f77b0c5ac8d3e5097142 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 8 Aug 2018 07:49:45 +1000 Subject: [PATCH 06/12] add file to command --- gitcommands.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitcommands.go b/gitcommands.go index f12545897..c8f81dd6b 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -279,7 +279,7 @@ func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) { func openFile(g *gocui.Gui, filename string) (string, error) { cmdName, cmdTrail := getOpenCommand() - return runCommand(cmdName + " " + cmdTrail) + return runCommand(cmdName + " " + filename + " " + cmdTrail) // TODO: find out why finder is being opened here } func gitAddPatch(g *gocui.Gui, filename string) { @@ -306,7 +306,7 @@ func getOpenCommand() (string, string) { "cygstart": "", } for name, trail := range trailMap { - if out, _ := runCommand("which " + name); out != "" { + if out, _ := runCommand("which " + name); out != "exit status 1" { return name, trail } } From 2f50cbf2b87f9ab607345bedae9dc50393c98dfc Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 8 Aug 2018 07:54:11 +1000 Subject: [PATCH 07/12] use gox default output --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c55390b48..d4f76e1c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,8 @@ script: # - go get -v ./... # - diff -u <(echo -n) <(gofmt -d .) # can't make gofmt ignore vendor directory # - go vet $(go list ./... | grep -v /vendor/) - - if [ "${LATEST}" = "true" ]; then gox -os="linux darwin windows" -arch="amd64" -output="lazygit.." -ldflags "-X main.Rev=`git rev-parse --short HEAD`" -verbose ./...; fi + - if [ "${LATEST}" = "true" ]; then gox -os="linux darwin windows" -arch="amd64" -ldflags "-X main.Rev=`git rev-parse --short HEAD`" -verbose ./...; fi + - ls - ls */* deploy: provider: releases From 92e75d4602b733e05b1dfdc774f1fc3e15f92cc8 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 8 Aug 2018 08:24:24 +1000 Subject: [PATCH 08/12] "step one towards dealing with gpgsign" --- files_panel.go | 2 +- gitcommands.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/files_panel.go b/files_panel.go index 401294933..a102399f7 100644 --- a/files_panel.go +++ b/files_panel.go @@ -176,7 +176,7 @@ func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { if 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) } refreshFiles(g) diff --git a/gitcommands.go b/gitcommands.go index c8f81dd6b..0f5692e87 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -447,7 +447,12 @@ func removeFile(file GitFile) error { return err } -func gitCommit(message string) (string, error) { +func gitCommit(g *gocui.Gui, message string) (string, error) { + out, _ := runDirectCommand("git config --get commit.gpgsign") + if out != "" { + runSubProcess(g, "git", "commit", "-m", "\""+message+"\"") + return "", nil + } return runDirectCommand("git commit -m \"" + message + "\"") } From 3839719154f885a77ffeb3957f86cf367b79d3a7 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 8 Aug 2018 19:18:41 +1000 Subject: [PATCH 09/12] "move keybindings into its own file" --- gui.go | 65 ----------------------------------------------- keybindings.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 65 deletions(-) create mode 100644 keybindings.go diff --git a/gui.go b/gui.go index 4d6b1d25c..fa820b36e 100644 --- a/gui.go +++ b/gui.go @@ -70,71 +70,6 @@ func handleRefresh(g *gocui.Gui, v *gocui.View) error { return refreshSidePanels(g) } -// Binding - a keybinding mapping a key and modifier to a handler. The keypress -// is only handled if the given view has focus, or handled globally if the view -// is "" -type Binding struct { - ViewName string - Handler func(*gocui.Gui, *gocui.View) error - Key interface{} // FIXME: find out how to get `gocui.Key | rune` - Modifier gocui.Modifier -} - -func keybindings(g *gocui.Gui) error { - bindings := []Binding{ - Binding{ViewName: "", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView}, - Binding{ViewName: "", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView}, - Binding{ViewName: "", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView}, - Binding{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit}, - Binding{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit}, - Binding{ViewName: "", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown}, - Binding{ViewName: "", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp}, - Binding{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain}, - Binding{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain}, - Binding{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles}, - Binding{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles}, - Binding{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh}, - Binding{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress}, - 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: '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: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, - Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, - Binding{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile}, - Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, - Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, - 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.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, - Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, - Binding{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk}, - Binding{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks}, - Binding{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, - Binding{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, - Binding{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot}, - Binding{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress}, - Binding{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName}, - Binding{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout}, - Binding{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch}, - Binding{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge}, - Binding{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown}, - Binding{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit}, - Binding{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit}, - Binding{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply}, - Binding{ViewName: "stash", Key: 'k', Modifier: gocui.ModNone, Handler: handleStashPop}, - Binding{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop}, - } - for _, binding := range bindings { - if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil { - return err - } - } - return nil -} - func layout(g *gocui.Gui) error { g.Highlight = true g.SelFgColor = gocui.ColorWhite | gocui.AttrBold diff --git a/keybindings.go b/keybindings.go new file mode 100644 index 000000000..6448a6be0 --- /dev/null +++ b/keybindings.go @@ -0,0 +1,68 @@ +package main + +import "github.com/jesseduffield/gocui" + +// Binding - a keybinding mapping a key and modifier to a handler. The keypress +// is only handled if the given view has focus, or handled globally if the view +// is "" +type Binding struct { + ViewName string + Handler func(*gocui.Gui, *gocui.View) error + Key interface{} // FIXME: find out how to get `gocui.Key | rune` + Modifier gocui.Modifier +} + +func keybindings(g *gocui.Gui) error { + bindings := []Binding{ + Binding{ViewName: "", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView}, + Binding{ViewName: "", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView}, + Binding{ViewName: "", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView}, + Binding{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit}, + Binding{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit}, + Binding{ViewName: "", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown}, + Binding{ViewName: "", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp}, + Binding{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain}, + Binding{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain}, + Binding{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles}, + Binding{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles}, + Binding{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh}, + Binding{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress}, + 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: '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: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, + Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, + Binding{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile}, + Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, + Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, + 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.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, + Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, + Binding{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk}, + Binding{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks}, + Binding{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, + Binding{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, + Binding{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot}, + Binding{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress}, + Binding{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName}, + Binding{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout}, + Binding{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch}, + Binding{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge}, + Binding{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown}, + Binding{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit}, + Binding{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit}, + Binding{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply}, + Binding{ViewName: "stash", Key: 'k', Modifier: gocui.ModNone, Handler: handleStashPop}, + Binding{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop}, + } + for _, binding := range bindings { + if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil { + return err + } + } + return nil +} From c9653337569f6a3ffab11733a86667a42a9e6bf3 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 8 Aug 2018 19:46:21 +1000 Subject: [PATCH 10/12] "opening and editing" --- files_panel.go | 4 ++-- gitcommands.go | 39 +++++++++++++++++++++++---------------- main.go | 10 ++++------ 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/files_panel.go b/files_panel.go index a102399f7..4104845fc 100644 --- a/files_panel.go +++ b/files_panel.go @@ -193,8 +193,8 @@ func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) } return nil } - if output, err := open(g, file.Name); err != nil { - return createErrorPanel(g, output) + if _, err := open(g, file.Name); err != nil { + return createErrorPanel(g, err.Error()) } return nil } diff --git a/gitcommands.go b/gitcommands.go index 0f5692e87..e4f500a8f 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -19,6 +19,9 @@ import ( var ( // ErrNoCheckedOutBranch : When we have no 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 @@ -278,8 +281,26 @@ func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) { } func openFile(g *gocui.Gui, filename string) (string, error) { - cmdName, cmdTrail := getOpenCommand() - return runCommand(cmdName + " " + filename + " " + cmdTrail) // TODO: find out why finder is being opened here + 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) { @@ -299,20 +320,6 @@ func editFile(g *gocui.Gui, filename string) (string, error) { return "", nil } -func getOpenCommand() (string, string) { - //NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX) - trailMap := map[string]string{ - "xdg-open": "&>/dev/null &", - "cygstart": "", - } - for name, trail := range trailMap { - if out, _ := runCommand("which " + name); out != "exit status 1" { - return name, trail - } - } - return "open", "" -} - func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) { subprocess = exec.Command(cmdName, commandArgs...) subprocess.Stdin = os.Stdin diff --git a/main.go b/main.go index aedc24b60..ad753c407 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,6 @@ var ( ErrSubprocess = errors.New("running subprocess") subprocess *exec.Cmd startTime time.Time - debugging bool // Rev - Git Revision Rev string @@ -27,9 +26,9 @@ var ( // Version - Version number Version = "unversioned" - builddate string - debuggingPointer = flag.Bool("debug", false, "a boolean") - versionFlag = flag.Bool("v", false, "Print the current version") + builddate string + debuggingFlag = flag.Bool("debug", false, "a boolean") + versionFlag = flag.Bool("v", false, "Print the current version") ) func homeDirectory() string { @@ -53,7 +52,7 @@ func commandLog(objects ...interface{}) { } func localLog(colour color.Attribute, path string, objects ...interface{}) { - if !debugging { + if !*debuggingFlag { return } f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) @@ -75,7 +74,6 @@ func navigateToRepoRootDirectory() { func main() { startTime = time.Now() - debugging = *debuggingPointer devLog("\n\n\n\n\n\n\n\n\n\n") flag.Parse() if *versionFlag { From 8013f18177bcad2053c2802be4724a900c751a85 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 8 Aug 2018 20:44:06 +1000 Subject: [PATCH 11/12] add test script that reads user input for subprocess testing --- test/shell_script_input_prompt.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 test/shell_script_input_prompt.sh diff --git a/test/shell_script_input_prompt.sh b/test/shell_script_input_prompt.sh new file mode 100755 index 000000000..eb3a31374 --- /dev/null +++ b/test/shell_script_input_prompt.sh @@ -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 From 4281cc2884b96d475c88fead179e25d890e1edd0 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 8 Aug 2018 20:45:12 +1000 Subject: [PATCH 12/12] add git config check and editing ability --- Gopkg.lock | 9 ++ gitcommands.go | 16 ++- vendor/github.com/tcnksm/go-gitconfig/LICENSE | 22 ++++ .../tcnksm/go-gitconfig/gitconfig.go | 113 ++++++++++++++++++ 4 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 vendor/github.com/tcnksm/go-gitconfig/LICENSE create mode 100644 vendor/github.com/tcnksm/go-gitconfig/gitconfig.go diff --git a/Gopkg.lock b/Gopkg.lock index 9a564e42a..c7f37e045 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -57,6 +57,14 @@ pruneopts = "NUT" revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3" +[[projects]] + digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541" + name = "github.com/tcnksm/go-gitconfig" + packages = ["."] + pruneopts = "NUT" + revision = "d154598bacbf4501c095a309753c5d4af66caa81" + version = "v0.1.2" + [[projects]] branch = "master" digest = "1:4d8a79fbc6fa348fc94afa4235947c5196b7900ed71b94aa5fcbc7e273d150e1" @@ -72,6 +80,7 @@ "github.com/fatih/color", "github.com/golang-collections/collections/stack", "github.com/jesseduffield/gocui", + "github.com/tcnksm/go-gitconfig", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/gitcommands.go b/gitcommands.go index e4f500a8f..2c5a4f2dc 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -14,6 +14,7 @@ import ( "github.com/fatih/color" "github.com/jesseduffield/gocui" + gitconfig "github.com/tcnksm/go-gitconfig" ) var ( @@ -267,6 +268,7 @@ func runCommand(command string) (string, error) { commandStartTime := time.Now() commandLog(command) splitCmd := strings.Split(command, " ") + devLog(splitCmd) cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() devLog("run command time: ", time.Now().Sub(commandStartTime)) return sanitisedCommandOutput(cmdOut, err) @@ -308,14 +310,16 @@ func gitAddPatch(g *gocui.Gui, filename string) { } func editFile(g *gocui.Gui, filename string) (string, error) { - editor := os.Getenv("VISUAL") + editor, _ := gitconfig.Global("core.editor") + if editor == "" { + editor = os.Getenv("VISUAL") + } if editor == "" { editor = os.Getenv("EDITOR") } if editor == "" { - editor = "vi" + return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.") } - runSubProcess(g, editor, filename) return "", nil } @@ -455,9 +459,9 @@ func removeFile(file GitFile) error { } func gitCommit(g *gocui.Gui, message string) (string, error) { - out, _ := runDirectCommand("git config --get commit.gpgsign") - if out != "" { - runSubProcess(g, "git", "commit", "-m", "\""+message+"\"") + gpgsign, _ := gitconfig.Global("commit.gpgsign") + if gpgsign != "" { + runSubProcess(g, "bash", "-c", "git commit -m \""+message+"\"") return "", nil } return runDirectCommand("git commit -m \"" + message + "\"") diff --git a/vendor/github.com/tcnksm/go-gitconfig/LICENSE b/vendor/github.com/tcnksm/go-gitconfig/LICENSE new file mode 100644 index 000000000..3457f2566 --- /dev/null +++ b/vendor/github.com/tcnksm/go-gitconfig/LICENSE @@ -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. \ No newline at end of file diff --git a/vendor/github.com/tcnksm/go-gitconfig/gitconfig.go b/vendor/github.com/tcnksm/go-gitconfig/gitconfig.go new file mode 100644 index 000000000..46c6a10f6 --- /dev/null +++ b/vendor/github.com/tcnksm/go-gitconfig/gitconfig.go @@ -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") +}