2018-05-26 05:23:39 +02:00
package main
import (
2018-07-21 07:51:18 +02:00
// "io"
// "io/ioutil"
2018-05-26 05:23:39 +02:00
2018-07-21 07:51:18 +02:00
// "strings"
2018-05-26 05:23:39 +02:00
2018-07-21 07:51:18 +02:00
"errors"
"strings"
2018-05-26 05:23:39 +02:00
2018-07-21 07:51:18 +02:00
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
2018-05-26 05:23:39 +02:00
)
2018-06-01 15:23:31 +02:00
var (
2018-08-09 07:24:12 +02:00
errNoFiles = errors . New ( "No changed files" )
errNoUsername = errors . New ( ` No username set. Please do: git config --global user.name "Your Name" ` )
2018-06-01 15:23:31 +02:00
)
2018-05-26 05:23:39 +02:00
func stagedFiles ( files [ ] GitFile ) [ ] GitFile {
2018-07-21 07:51:18 +02:00
result := make ( [ ] GitFile , 0 )
for _ , file := range files {
if file . HasStagedChanges {
result = append ( result , file )
}
}
return result
2018-05-26 05:23:39 +02:00
}
2018-07-21 09:48:27 +02:00
func stageSelectedFile ( g * gocui . Gui ) error {
file , err := getSelectedFile ( g )
if err != nil {
return err
}
return stageFile ( file . Name )
}
2018-05-26 05:23:39 +02:00
func handleFilePress ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
file , err := getSelectedFile ( g )
if err != nil {
2018-08-09 07:24:12 +02:00
if err == errNoFiles {
2018-08-05 10:34:50 +02:00
return nil
}
2018-07-21 07:51:18 +02:00
return err
}
2018-05-26 05:23:39 +02:00
2018-07-21 09:48:27 +02:00
if file . HasMergeConflicts {
return handleSwitchToMerge ( g , v )
}
2018-07-21 07:51:18 +02:00
if file . HasUnstagedChanges {
stageFile ( file . Name )
} else {
unStageFile ( file . Name , file . Tracked )
}
2018-05-26 05:23:39 +02:00
2018-07-21 07:51:18 +02:00
if err := refreshFiles ( g ) ; err != nil {
return err
}
2018-05-26 05:23:39 +02:00
2018-08-06 07:41:29 +02:00
return handleFileSelect ( g , v )
2018-05-26 05:23:39 +02:00
}
2018-08-07 11:50:35 +02:00
func handleAddPatch ( g * gocui . Gui , v * gocui . View ) error {
file , err := getSelectedFile ( g )
if err != nil {
2018-08-09 07:24:12 +02:00
if err == errNoFiles {
2018-08-07 11:50:35 +02:00
return nil
}
return err
}
2018-08-08 12:50:45 +02:00
if ! file . HasUnstagedChanges {
return createErrorPanel ( g , "File has no unstaged changes to add" )
}
2018-08-09 01:33:18 +02:00
if ! file . Tracked {
return createErrorPanel ( g , "Cannot git add --patch untracked files" )
}
2018-08-07 11:50:35 +02:00
gitAddPatch ( g , file . Name )
return err
}
2018-06-09 11:06:33 +02:00
func getSelectedFile ( g * gocui . Gui ) ( GitFile , error ) {
2018-07-21 07:51:18 +02:00
if len ( state . GitFiles ) == 0 {
2018-08-09 07:24:12 +02:00
return GitFile { } , errNoFiles
2018-07-21 07:51:18 +02:00
}
filesView , err := g . View ( "files" )
if err != nil {
panic ( err )
}
lineNumber := getItemPosition ( filesView )
return state . GitFiles [ lineNumber ] , nil
2018-05-26 05:23:39 +02:00
}
func handleFileRemove ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 09:48:27 +02:00
file , err := getSelectedFile ( g )
if err != nil {
2018-08-09 07:24:12 +02:00
if err == errNoFiles {
2018-08-07 04:51:03 +02:00
return nil
}
2018-07-21 09:48:27 +02:00
return err
}
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 )
2018-05-26 05:23:39 +02:00
}
2018-06-01 15:23:31 +02:00
func handleIgnoreFile ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
file , err := getSelectedFile ( g )
if err != nil {
2018-08-07 13:04:41 +02:00
return createErrorPanel ( g , err . Error ( ) )
2018-07-21 07:51:18 +02:00
}
if file . Tracked {
return createErrorPanel ( g , "Cannot ignore tracked files" )
}
gitIgnore ( file . Name )
return refreshFiles ( g )
2018-06-01 15:23:31 +02:00
}
2018-06-09 11:06:33 +02:00
func renderfilesOptions ( g * gocui . Gui , gitFile * GitFile ) error {
2018-07-21 07:51:18 +02:00
optionsMap := map [ string ] string {
2018-08-09 15:34:50 +02:00
"← → ↑ ↓" : "navigate" ,
"S" : "stash files" ,
"c" : "commit changes" ,
"o" : "open" ,
"i" : "ignore" ,
"d" : "delete" ,
"space" : "toggle staged" ,
"R" : "refresh" ,
"t" : "add patch" ,
"e" : "edit" ,
"PgUp/PgDn" : "scroll" ,
2018-07-21 07:51:18 +02:00
}
if state . HasMergeConflicts {
optionsMap [ "a" ] = "abort merge"
optionsMap [ "m" ] = "resolve merge conflicts"
}
if gitFile == nil {
return renderOptionsMap ( g , optionsMap )
}
if gitFile . Tracked {
optionsMap [ "d" ] = "checkout"
}
return renderOptionsMap ( g , optionsMap )
2018-06-09 11:06:33 +02:00
}
2018-05-26 05:23:39 +02:00
func handleFileSelect ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
gitFile , err := getSelectedFile ( g )
if err != nil {
2018-08-09 07:24:12 +02:00
if err != errNoFiles {
2018-07-21 07:51:18 +02:00
return err
}
renderString ( g , "main" , "No changed files" )
return renderfilesOptions ( g , nil )
}
renderfilesOptions ( g , & gitFile )
var content string
if gitFile . HasMergeConflicts {
return refreshMergePanel ( g )
}
2018-06-09 11:06:33 +02:00
2018-07-21 07:51:18 +02:00
content = getDiff ( gitFile )
return renderString ( g , "main" , content )
2018-05-26 05:23:39 +02:00
}
2018-06-05 10:49:10 +02:00
func handleCommitPress ( g * gocui . Gui , filesView * gocui . View ) error {
2018-07-21 07:51:18 +02:00
if len ( stagedFiles ( state . GitFiles ) ) == 0 && ! state . HasMergeConflicts {
return createErrorPanel ( g , "There are no staged files to commit" )
}
2018-08-11 07:09:37 +02:00
commitMessageView := getCommitMessageView ( g )
g . Update ( func ( g * gocui . Gui ) error {
g . SetViewOnTop ( "commitMessage" )
switchFocus ( g , filesView , commitMessageView )
return nil
2018-07-21 07:51:18 +02:00
} )
return nil
2018-06-05 10:49:10 +02:00
}
2018-08-06 15:29:00 +02:00
func genericFileOpen ( g * gocui . Gui , v * gocui . View , open func ( * gocui . Gui , string ) ( string , error ) ) error {
2018-07-21 07:51:18 +02:00
file , err := getSelectedFile ( g )
if err != nil {
2018-08-09 07:24:12 +02:00
if err != errNoFiles {
2018-08-05 11:54:09 +02:00
return err
}
return nil
2018-07-21 07:51:18 +02:00
}
2018-08-08 11:46:21 +02:00
if _ , err := open ( g , file . Name ) ; err != nil {
return createErrorPanel ( g , err . Error ( ) )
2018-08-06 05:49:15 +02:00
}
return nil
2018-05-26 05:23:39 +02:00
}
2018-08-06 15:29:00 +02:00
func handleFileEdit ( g * gocui . Gui , v * gocui . View ) error {
return genericFileOpen ( g , v , editFile )
}
2018-06-01 15:23:31 +02:00
func handleFileOpen ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
return genericFileOpen ( g , v , openFile )
2018-06-01 15:23:31 +02:00
}
2018-08-06 15:29:00 +02:00
2018-05-26 05:23:39 +02:00
func handleSublimeFileOpen ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
return genericFileOpen ( g , v , sublimeOpenFile )
}
2018-08-06 15:29:00 +02:00
2018-07-21 07:51:18 +02:00
func handleVsCodeFileOpen ( g * gocui . Gui , v * gocui . View ) error {
return genericFileOpen ( g , v , vsCodeOpenFile )
2018-05-26 05:23:39 +02:00
}
2018-06-10 03:52:30 +02:00
func handleRefreshFiles ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 09:48:27 +02:00
return refreshFiles ( g )
2018-06-10 03:52:30 +02:00
}
2018-06-09 11:06:33 +02:00
func refreshStateGitFiles ( ) {
2018-07-21 07:51:18 +02:00
// get files to stage
gitFiles := getGitStatusFiles ( )
state . GitFiles = mergeGitStatusFiles ( state . GitFiles , gitFiles )
updateHasMergeConflictStatus ( )
2018-06-09 11:06:33 +02:00
}
2018-05-26 05:23:39 +02:00
2018-06-09 11:06:33 +02:00
func updateHasMergeConflictStatus ( ) error {
2018-07-21 07:51:18 +02:00
merging , err := isInMergeState ( )
if err != nil {
return err
}
state . HasMergeConflicts = merging
return nil
2018-06-09 11:06:33 +02:00
}
func renderGitFile ( gitFile GitFile , filesView * gocui . View ) {
2018-07-21 07:51:18 +02:00
// potentially inefficient to be instantiating these color
// objects with each render
red := color . New ( color . FgRed )
green := color . New ( color . FgGreen )
if ! gitFile . Tracked && ! gitFile . HasStagedChanges {
red . Fprintln ( filesView , gitFile . DisplayString )
return
}
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 )
}
2018-06-09 11:06:33 +02:00
}
func catSelectedFile ( g * gocui . Gui ) ( string , error ) {
2018-07-21 07:51:18 +02:00
item , err := getSelectedFile ( g )
if err != nil {
2018-08-09 07:24:12 +02:00
if err != errNoFiles {
2018-07-21 07:51:18 +02:00
return "" , err
}
return "" , renderString ( g , "main" , "No file to display" )
}
cat , err := catFile ( item . Name )
if err != nil {
panic ( err )
}
return cat , nil
2018-06-09 11:06:33 +02:00
}
func refreshFiles ( g * gocui . Gui ) error {
2018-07-21 07:51:18 +02:00
filesView , err := g . View ( "files" )
if err != nil {
return err
}
refreshStateGitFiles ( )
filesView . Clear ( )
for _ , gitFile := range state . GitFiles {
renderGitFile ( gitFile , filesView )
}
correctCursor ( filesView )
if filesView == g . CurrentView ( ) {
handleFileSelect ( g , filesView )
}
return nil
2018-05-26 05:23:39 +02:00
}
2018-05-26 07:44:44 +02:00
2018-05-27 08:32:09 +02:00
func pullFiles ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
createMessagePanel ( g , v , "" , "Pulling..." )
go func ( ) {
if output , err := gitPull ( ) ; err != nil {
createErrorPanel ( g , output )
} else {
closeConfirmationPrompt ( g )
refreshCommits ( g )
refreshStatus ( g )
}
2018-07-21 07:57:23 +02:00
refreshFiles ( g )
2018-07-21 07:51:18 +02:00
} ( )
return nil
2018-05-26 07:44:44 +02:00
}
2018-05-27 08:32:09 +02:00
func pushFiles ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
createMessagePanel ( g , v , "" , "Pushing..." )
go func ( ) {
if output , err := gitPush ( ) ; err != nil {
createErrorPanel ( g , output )
} else {
closeConfirmationPrompt ( g )
refreshCommits ( g )
refreshStatus ( g )
}
} ( )
return nil
2018-05-27 08:32:09 +02:00
}
2018-06-09 11:06:33 +02:00
func handleSwitchToMerge ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
mergeView , err := g . View ( "main" )
if err != nil {
return err
}
file , err := getSelectedFile ( g )
if err != nil {
2018-08-09 07:24:12 +02:00
if err != errNoFiles {
2018-07-21 07:51:18 +02:00
return err
}
return nil
}
if ! file . HasMergeConflicts {
2018-07-21 09:48:27 +02:00
return createErrorPanel ( g , "This file has no merge conflicts" )
2018-07-21 07:51:18 +02:00
}
switchFocus ( g , v , mergeView )
return refreshMergePanel ( g )
2018-06-09 11:06:33 +02:00
}
func handleAbortMerge ( g * gocui . Gui , v * gocui . View ) error {
2018-07-21 07:51:18 +02:00
output , err := gitAbortMerge ( )
if err != nil {
return createErrorPanel ( g , output )
}
createMessagePanel ( g , v , "" , "Merge aborted" )
refreshStatus ( g )
return refreshFiles ( g )
2018-06-09 11:06:33 +02:00
}
2018-08-09 11:32:56 +02:00
func handleResetHard ( g * gocui . Gui , v * gocui . View ) error {
return createConfirmationPanel ( g , v , "Clear file panel" , "Are you sure you want `reset --hard HEAD`? You may lose changes" , func ( g * gocui . Gui , v * gocui . View ) error {
if err := gitResetHard ( ) ; err != nil {
createErrorPanel ( g , err . Error ( ) )
}
return refreshFiles ( g )
} , nil )
}