2018-08-12 11:31:27 +02:00
package app
import (
2019-03-16 02:31:09 +02:00
"bufio"
2020-08-27 14:19:03 +02:00
"errors"
2019-03-02 11:32:16 +02:00
"fmt"
2021-10-23 00:52:19 +02:00
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
2021-06-15 09:31:52 +02:00
"github.com/aybabtme/humanlog"
2018-08-12 11:31:27 +02:00
"github.com/jesseduffield/lazygit/pkg/commands"
2021-10-23 00:52:19 +02:00
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
2020-09-29 11:10:57 +02:00
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
2021-12-29 02:37:15 +02:00
"github.com/jesseduffield/lazygit/pkg/common"
2018-08-12 11:31:27 +02:00
"github.com/jesseduffield/lazygit/pkg/config"
2020-09-27 08:17:26 +02:00
"github.com/jesseduffield/lazygit/pkg/env"
2018-08-13 12:26:02 +02:00
"github.com/jesseduffield/lazygit/pkg/gui"
2018-08-14 15:26:25 +02:00
"github.com/jesseduffield/lazygit/pkg/i18n"
2018-08-19 15:28:29 +02:00
"github.com/jesseduffield/lazygit/pkg/updates"
2018-08-25 07:55:49 +02:00
"github.com/sirupsen/logrus"
2018-08-12 11:31:27 +02:00
)
// App struct
type App struct {
2021-12-29 02:37:15 +02:00
* common . Common
closers [ ] io . Closer
2019-03-03 03:44:10 +02:00
Config config . AppConfigurer
2020-09-29 11:10:57 +02:00
OSCommand * oscommands . OSCommand
2019-03-03 03:44:10 +02:00
Gui * gui . Gui
Updater * updates . Updater // may only need this on the Gui
ClientContext string
2018-08-13 13:16:21 +02:00
}
2019-07-07 20:15:11 +02:00
type errorMapping struct {
originalError string
newError string
}
2022-01-08 06:46:35 +02:00
func newProductionLogger ( ) * logrus . Logger {
2018-08-13 13:16:21 +02:00
log := logrus . New ( )
2018-08-26 07:46:18 +02:00
log . Out = ioutil . Discard
2019-03-16 01:37:31 +02:00
log . SetLevel ( logrus . ErrorLevel )
2018-08-26 07:46:18 +02:00
return log
}
2019-03-16 01:37:31 +02:00
func getLogLevel ( ) logrus . Level {
strLevel := os . Getenv ( "LOG_LEVEL" )
level , err := logrus . ParseLevel ( strLevel )
if err != nil {
return logrus . DebugLevel
}
return level
}
2022-01-08 06:46:35 +02:00
func newDevelopmentLogger ( ) * logrus . Logger {
2020-10-03 06:54:55 +02:00
logger := logrus . New ( )
logger . SetLevel ( getLogLevel ( ) )
logPath , err := config . LogPath ( )
if err != nil {
log . Fatal ( err )
}
2022-03-19 00:38:49 +02:00
file , err := os . OpenFile ( logPath , os . O_CREATE | os . O_WRONLY | os . O_APPEND , 0 o666 )
2018-08-13 13:16:21 +02:00
if err != nil {
2022-01-09 05:09:15 +02:00
log . Fatalf ( "Unable to log to log file: %v" , err )
2018-08-13 13:16:21 +02:00
}
2020-10-03 06:54:55 +02:00
logger . SetOutput ( file )
return logger
2018-08-12 11:31:27 +02:00
}
2018-08-26 07:46:18 +02:00
func newLogger ( config config . AppConfigurer ) * logrus . Entry {
var log * logrus . Logger
2019-02-18 12:29:43 +02:00
if config . GetDebug ( ) || os . Getenv ( "DEBUG" ) == "TRUE" {
2022-01-08 06:46:35 +02:00
log = newDevelopmentLogger ( )
2018-08-26 07:46:18 +02:00
} else {
2022-01-08 06:46:35 +02:00
log = newProductionLogger ( )
2018-08-26 07:46:18 +02:00
}
2018-12-07 09:52:31 +02:00
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
2018-12-04 10:50:11 +02:00
log . Formatter = & logrus . JSONFormatter { }
2018-08-26 07:46:18 +02:00
return log . WithFields ( logrus . Fields {
"debug" : config . GetDebug ( ) ,
"version" : config . GetVersion ( ) ,
"commit" : config . GetCommit ( ) ,
"buildDate" : config . GetBuildDate ( ) ,
} )
}
2019-02-18 10:42:23 +02:00
// NewApp bootstrap a new application
2022-01-31 13:11:34 +02:00
func NewApp ( config config . AppConfigurer ) ( * App , error ) {
2021-12-29 03:03:35 +02:00
userConfig := config . GetUserConfig ( )
2018-08-12 11:31:27 +02:00
app := & App {
closers : [ ] io . Closer { } ,
Config : config ,
}
var err error
2021-12-29 02:37:15 +02:00
log := newLogger ( config )
2021-12-29 03:03:35 +02:00
tr , err := i18n . NewTranslationSetFromConfig ( log , userConfig . Gui . Language )
2021-08-27 09:00:53 +02:00
if err != nil {
return app , err
}
2018-08-14 14:12:07 +02:00
2021-12-29 02:37:15 +02:00
app . Common = & common . Common {
Log : log ,
Tr : tr ,
2021-12-29 03:03:35 +02:00
UserConfig : userConfig ,
2021-12-29 02:37:15 +02:00
Debug : config . GetDebug ( ) ,
}
2019-02-18 12:29:43 +02:00
// if we are being called in 'demon' mode, we can just return here
2019-03-03 03:44:10 +02:00
app . ClientContext = os . Getenv ( "LAZYGIT_CLIENT_COMMAND" )
if app . ClientContext != "" {
2019-02-18 12:29:43 +02:00
return app , nil
}
2022-01-02 01:34:33 +02:00
app . OSCommand = oscommands . NewOSCommand ( app . Common , oscommands . GetPlatform ( ) , oscommands . NewNullGuiIO ( log ) )
2019-02-18 12:29:43 +02:00
2021-12-29 03:03:35 +02:00
app . Updater , err = updates . NewUpdater ( app . Common , config , app . OSCommand )
2018-08-12 11:31:27 +02:00
if err != nil {
2018-08-18 11:43:58 +02:00
return app , err
2018-08-12 11:31:27 +02:00
}
2019-03-16 02:31:09 +02:00
2022-03-15 15:12:26 +02:00
dirName , err := os . Getwd ( )
if err != nil {
return app , err
}
2020-08-16 14:49:37 +02:00
showRecentRepos , err := app . setupRepo ( )
if err != nil {
2019-03-16 02:31:09 +02:00
return app , err
}
2022-01-02 01:34:33 +02:00
gitConfig := git_config . NewStdCachedGitConfig ( app . Log )
2020-09-27 07:36:04 +02:00
2022-01-31 13:11:34 +02:00
app . Gui , err = gui . NewGui ( app . Common , config , gitConfig , app . Updater , showRecentRepos , dirName )
2018-08-13 12:26:02 +02:00
if err != nil {
2018-08-18 11:43:58 +02:00
return app , err
2018-08-13 12:26:02 +02:00
}
2018-08-12 11:31:27 +02:00
return app , nil
}
2020-08-27 14:19:03 +02:00
func ( app * App ) validateGitVersion ( ) error {
2021-12-29 05:33:38 +02:00
output , err := app . OSCommand . Cmd . New ( "git --version" ) . RunWithOutput ( )
2020-08-27 14:19:03 +02:00
// if we get an error anywhere here we'll show the same status
2020-10-04 02:00:48 +02:00
minVersionError := errors . New ( app . Tr . MinGitVersionError )
2020-08-27 14:19:03 +02:00
if err != nil {
return minVersionError
}
2020-09-18 13:00:03 +02:00
if isGitVersionValid ( output ) {
return nil
}
return minVersionError
}
func isGitVersionValid ( versionStr string ) bool {
// output should be something like: 'git version 2.23.0 (blah)'
re := regexp . MustCompile ( ` [^\d]+([\d\.]+) ` )
matches := re . FindStringSubmatch ( versionStr )
if len ( matches ) == 0 {
return false
}
gitVersion := matches [ 1 ]
2020-08-27 14:19:03 +02:00
majorVersion , err := strconv . Atoi ( gitVersion [ 0 : 1 ] )
if err != nil {
2020-09-18 13:00:03 +02:00
return false
2020-08-27 14:19:03 +02:00
}
if majorVersion < 2 {
2020-09-18 13:00:03 +02:00
return false
2020-08-27 14:19:03 +02:00
}
2020-09-18 13:00:03 +02:00
return true
2020-08-27 14:19:03 +02:00
}
2020-08-16 14:49:37 +02:00
func ( app * App ) setupRepo ( ) ( bool , error ) {
2020-08-27 14:19:03 +02:00
if err := app . validateGitVersion ( ) ; err != nil {
return false , err
}
2020-09-27 08:17:26 +02:00
if env . GetGitDirEnv ( ) != "" {
2022-01-08 05:10:01 +02:00
// we've been given the git dir directly. We'll verify this dir when initializing our Git object
2020-09-27 07:36:04 +02:00
return false , nil
}
2019-03-16 02:31:09 +02:00
// if we are not in a git repo, we ask if we want to `git init`
2021-03-30 13:17:42 +02:00
if err := commands . VerifyInGitRepo ( app . OSCommand ) ; err != nil {
2020-05-28 06:20:13 +02:00
cwd , err := os . Getwd ( )
if err != nil {
2020-08-16 14:49:37 +02:00
return false , err
2019-03-16 02:31:09 +02:00
}
2020-05-28 06:20:13 +02:00
info , _ := os . Stat ( filepath . Join ( cwd , ".git" ) )
if info != nil && info . IsDir ( ) {
2020-08-16 14:49:37 +02:00
return false , err // Current directory appears to be a git repository.
2020-05-28 06:20:13 +02:00
}
2020-11-24 13:21:11 +02:00
shouldInitRepo := true
2021-12-29 03:03:35 +02:00
notARepository := app . UserConfig . NotARepository
2020-11-24 14:02:31 +02:00
if notARepository == "prompt" {
2020-11-24 13:21:11 +02:00
// Offer to initialize a new repository in current directory.
fmt . Print ( app . Tr . CreateRepo )
response , _ := bufio . NewReader ( os . Stdin ) . ReadString ( '\n' )
if strings . Trim ( response , " \n" ) != "y" {
shouldInitRepo = false
}
2020-11-24 14:02:31 +02:00
} else if notARepository == "skip" {
2020-11-24 13:21:11 +02:00
shouldInitRepo = false
}
if ! shouldInitRepo {
2020-08-16 14:49:37 +02:00
// check if we have a recent repo we can open
recentRepos := app . Config . GetAppState ( ) . RecentRepos
if len ( recentRepos ) > 0 {
var err error
// try opening each repo in turn, in case any have been deleted
for _ , repoDir := range recentRepos {
if err = os . Chdir ( repoDir ) ; err == nil {
return true , nil
}
}
return false , err
}
2019-03-16 02:31:09 +02:00
os . Exit ( 1 )
}
2021-12-29 05:33:38 +02:00
if err := app . OSCommand . Cmd . New ( "git init" ) . Run ( ) ; err != nil {
2020-08-16 14:49:37 +02:00
return false , err
2019-03-16 02:31:09 +02:00
}
}
2021-03-30 13:17:42 +02:00
2020-08-16 14:49:37 +02:00
return false , nil
2019-03-16 02:31:09 +02:00
}
2022-01-31 13:11:34 +02:00
func ( app * App ) Run ( filterPath string ) error {
2019-03-03 03:44:10 +02:00
if app . ClientContext == "INTERACTIVE_REBASE" {
2019-02-18 12:29:43 +02:00
return app . Rebase ( )
}
2019-03-03 03:44:10 +02:00
if app . ClientContext == "EXIT_IMMEDIATELY" {
os . Exit ( 0 )
}
2022-01-31 13:11:34 +02:00
err := app . Gui . RunAndHandleError ( filterPath )
2019-07-07 20:15:11 +02:00
return err
2019-02-18 10:42:23 +02:00
}
2020-09-27 07:36:04 +02:00
func gitDir ( ) string {
2020-09-27 08:17:26 +02:00
dir := env . GetGitDirEnv ( )
2020-09-27 07:36:04 +02:00
if dir == "" {
return ".git"
}
return dir
}
2019-02-19 14:36:29 +02:00
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
2019-02-18 12:29:43 +02:00
func ( app * App ) Rebase ( ) error {
2019-02-19 14:36:29 +02:00
app . Log . Info ( "Lazygit invoked as interactive rebase demon" )
app . Log . Info ( "args: " , os . Args )
if strings . HasSuffix ( os . Args [ 1 ] , "git-rebase-todo" ) {
2022-03-19 00:38:49 +02:00
if err := ioutil . WriteFile ( os . Args [ 1 ] , [ ] byte ( os . Getenv ( "LAZYGIT_REBASE_TODO" ) ) , 0 o644 ) ; err != nil {
2019-03-02 04:22:02 +02:00
return err
}
2020-09-27 07:36:04 +02:00
} else if strings . HasSuffix ( os . Args [ 1 ] , filepath . Join ( gitDir ( ) , "COMMIT_EDITMSG" ) ) { // TODO: test
2019-02-19 14:36:29 +02:00
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
app . Log . Info ( "Lazygit demon did not match on any use cases" )
}
2019-02-18 12:29:43 +02:00
return nil
}
2018-08-12 11:31:27 +02:00
// Close closes any resources
func ( app * App ) Close ( ) error {
for _ , closer := range app . closers {
err := closer . Close ( )
if err != nil {
return err
}
}
return nil
}
2019-07-07 20:15:11 +02:00
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
func ( app * App ) KnownError ( err error ) ( string , bool ) {
errorMessage := err . Error ( )
2020-10-04 02:00:48 +02:00
knownErrorMessages := [ ] string { app . Tr . MinGitVersionError }
2020-08-27 14:19:03 +02:00
for _ , message := range knownErrorMessages {
if errorMessage == message {
return message , true
}
}
2019-07-07 20:15:11 +02:00
mappings := [ ] errorMapping {
{
2020-08-16 14:49:37 +02:00
originalError : "fatal: not a git repository" ,
2020-10-04 02:00:48 +02:00
newError : app . Tr . NotARepository ,
2019-07-07 20:15:11 +02:00
} ,
}
for _ , mapping := range mappings {
if strings . Contains ( errorMessage , mapping . originalError ) {
return mapping . newError , true
}
}
return "" , false
}
2021-06-15 09:31:52 +02:00
func TailLogs ( ) {
logFilePath , err := config . LogPath ( )
if err != nil {
log . Fatal ( err )
}
fmt . Printf ( "Tailing log file %s\n\n" , logFilePath )
opts := humanlog . DefaultOptions
opts . Truncates = false
_ , err = os . Stat ( logFilePath )
if err != nil {
if os . IsNotExist ( err ) {
log . Fatal ( "Log file does not exist. Run `lazygit --debug` first to create the log file" )
}
log . Fatal ( err )
}
TailLogsForPlatform ( logFilePath , opts )
}