1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-04 10:34:55 +02:00
lazygit/pkg/app/app.go
2021-04-06 19:34:32 +10:00

348 lines
8.2 KiB
Go

package app
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/aybabtme/humanlog"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/sirupsen/logrus"
)
// App struct
type App struct {
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *oscommands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.TranslationSet
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
type errorMapping struct {
originalError string
newError string
}
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
log.SetLevel(logrus.ErrorLevel)
return log
}
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
if err != nil {
return logrus.DebugLevel
}
return level
}
func newDevelopmentLogger(configurer config.AppConfigurer) *logrus.Logger {
logger := logrus.New()
logger.SetLevel(getLogLevel())
logPath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
}
logger.SetOutput(file)
return logger
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
log = newDevelopmentLogger(config)
} else {
log = newProductionLogger(config)
}
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
"commit": config.GetCommit(),
"buildDate": config.GetBuildDate(),
})
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewTranslationSet(app.Log)
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
}
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
if err != nil {
return app, err
}
showRecentRepos, err := app.setupRepo()
if err != nil {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
if err != nil {
return app, err
}
return app, nil
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
if err != nil {
return minVersionError
}
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]
majorVersion, err := strconv.Atoi(gitVersion[0:1])
if err != nil {
return false
}
if majorVersion < 2 {
return false
}
return true
}
func (app *App) setupRepo() (bool, error) {
if err := app.validateGitVersion(); err != nil {
return false, err
}
if env.GetGitDirEnv() != "" {
// we've been given the git dir directly. We'll verify this dir when initializing our GitCommand object
return false, nil
}
// if we are not in a git repo, we ask if we want to `git init`
if err := commands.VerifyInGitRepo(app.OSCommand); err != nil {
cwd, err := os.Getwd()
if err != nil {
return false, err
}
info, _ := os.Stat(filepath.Join(cwd, ".git"))
if info != nil && info.IsDir() {
return false, err // Current directory appears to be a git repository.
}
shouldInitRepo := true
notARepository := app.Config.GetUserConfig().NotARepository
if notARepository == "prompt" {
// 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
}
} else if notARepository == "skip" {
shouldInitRepo = false
}
if !shouldInitRepo {
// 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
}
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
return false, err
}
}
return false, nil
}
func (app *App) Run() error {
if app.ClientContext == "INTERACTIVE_REBASE" {
return app.Rebase()
}
if app.ClientContext == "EXIT_IMMEDIATELY" {
os.Exit(0)
}
err := app.Gui.RunAndHandleError()
return err
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
// 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
func (app *App) Rebase() error {
app.Log.Info("Lazygit invoked as interactive rebase demon")
app.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
return err
}
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// 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")
}
return nil
}
// Close closes any resources
func (app *App) Close() error {
for _, closer := range app.closers {
err := closer.Close()
if err != nil {
return err
}
}
return nil
}
// 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()
knownErrorMessages := []string{app.Tr.MinGitVersionError}
for _, message := range knownErrorMessages {
if errorMessage == message {
return message, true
}
}
mappings := []errorMapping{
{
originalError: "fatal: not a git repository",
newError: app.Tr.NotARepository,
},
}
for _, mapping := range mappings {
if strings.Contains(errorMessage, mapping.originalError) {
return mapping.newError, true
}
}
return "", false
}
func TailLogs() {
logFilePath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Tailing log file %s\n\n", logFilePath)
_, 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)
}
cmd := secureexec.Command("tail", "-f", logFilePath)
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
opts := humanlog.DefaultOptions
opts.Truncates = false
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
log.Fatal(err)
}
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
os.Exit(0)
}