From cd5b041b0f8b95b473849d222b71aa2fe3b24899 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 7 May 2022 15:23:08 +1000 Subject: [PATCH 1/2] clearer separation of concerns when bootstrapping application --- main.go | 29 ++- pkg/app/app.go | 201 ++++-------------- pkg/app/daemon/daemon.go | 107 ++++++++++ pkg/app/errors.go | 39 ++++ pkg/app/logging.go | 68 ++++-- pkg/cheatsheet/generate.go | 6 +- pkg/commands/git_commands/rebase.go | 7 +- pkg/commands/git_commands/rebase_test.go | 3 +- pkg/env/env.go | 6 +- pkg/logs/logs.go | 34 +++ pkg/logs/logs_default.go | 31 +++ .../logs_windows.go} | 2 +- 12 files changed, 328 insertions(+), 205 deletions(-) create mode 100644 pkg/app/daemon/daemon.go create mode 100644 pkg/app/errors.go create mode 100644 pkg/logs/logs.go create mode 100644 pkg/logs/logs_default.go rename pkg/{app/logging_windows.go => logs/logs_windows.go} (99%) diff --git a/main.go b/main.go index 7ed434377..c1de86e30 100644 --- a/main.go +++ b/main.go @@ -8,12 +8,12 @@ import ( "path/filepath" "runtime" - "github.com/go-errors/errors" "github.com/integrii/flaggy" "github.com/jesseduffield/lazygit/pkg/app" + "github.com/jesseduffield/lazygit/pkg/app/daemon" "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/constants" "github.com/jesseduffield/lazygit/pkg/env" + "github.com/jesseduffield/lazygit/pkg/logs" yaml "github.com/jesseduffield/yaml" ) @@ -117,7 +117,7 @@ func main() { } if logFlag { - app.TailLogs() + logs.TailLogs() os.Exit(0) } @@ -138,20 +138,15 @@ func main() { log.Fatal(err.Error()) } - app, err := app.NewApp(appConfig) - - if err == nil { - err = app.Run(filterPath) - } - + common, err := app.NewCommon(appConfig) if err != nil { - if errorMessage, known := app.KnownError(err); known { - log.Fatal(errorMessage) - } - newErr := errors.Wrap(err, 0) - stackTrace := newErr.ErrorStack() - app.Log.Error(stackTrace) - - log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace)) + log.Fatal(err) } + + if daemon.InDaemonMode() { + daemon.Handle(common) + return + } + + app.Run(appConfig, common, filterPath) } diff --git a/pkg/app/app.go b/pkg/app/app.go index 592505d60..d6f4c1928 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2,10 +2,8 @@ package app import ( "bufio" - "errors" "fmt" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -13,118 +11,81 @@ import ( "strconv" "strings" - "github.com/aybabtme/humanlog" + "github.com/go-errors/errors" + "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/constants" "github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/updates" - "github.com/sirupsen/logrus" ) -// App struct +// App is the struct that's instantiated from within main.go and it manages +// bootstrapping and running the application. type App struct { *common.Common - closers []io.Closer - Config config.AppConfigurer - OSCommand *oscommands.OSCommand - Gui *gui.Gui - Updater *updates.Updater // may only need this on the Gui - ClientContext string + closers []io.Closer + Config config.AppConfigurer + OSCommand *oscommands.OSCommand + Gui *gui.Gui + Updater *updates.Updater // may only need this on the Gui } -type errorMapping struct { - originalError string - newError string -} +func Run(config config.AppConfigurer, common *common.Common, filterPath string) { + app, err := NewApp(config, common) -func newProductionLogger() *logrus.Logger { - log := logrus.New() - log.Out = ioutil.Discard - log.SetLevel(logrus.ErrorLevel) - return log -} + if err == nil { + err = app.Run(filterPath) + } -func getLogLevel() logrus.Level { - strLevel := os.Getenv("LOG_LEVEL") - level, err := logrus.ParseLevel(strLevel) if err != nil { - return logrus.DebugLevel + if errorMessage, known := knownError(common.Tr, err); known { + log.Fatal(errorMessage) + } + newErr := errors.Wrap(err, 0) + stackTrace := newErr.ErrorStack() + app.Log.Error(stackTrace) + + log.Fatal(fmt.Sprintf("%s: %s\n\n%s", common.Tr.ErrorOccurred, constants.Links.Issues, stackTrace)) } - return level } -func newDevelopmentLogger() *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, 0o666) - if err != nil { - log.Fatalf("Unable to log to log file: %v", err) - } - 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() - } else { - log = newProductionLogger() - } - - // 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) (*App, error) { +func NewCommon(config config.AppConfigurer) (*common.Common, error) { userConfig := config.GetUserConfig() - app := &App{ - closers: []io.Closer{}, - Config: config, - } var err error log := newLogger(config) tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language) if err != nil { - return app, err + return nil, err } - app.Common = &common.Common{ + return &common.Common{ Log: log, Tr: tr, UserConfig: userConfig, Debug: config.GetDebug(), + }, nil +} + +// NewApp bootstrap a new application +func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) { + app := &App{ + closers: []io.Closer{}, + Config: config, + Common: common, } - // 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(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log)) - app.OSCommand = oscommands.NewOSCommand(app.Common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(log)) - - app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand) + var err error + app.Updater, err = updates.NewUpdater(common, config, app.OSCommand) if err != nil { return app, err } @@ -141,7 +102,7 @@ func NewApp(config config.AppConfigurer) (*App, error) { gitConfig := git_config.NewStdCachedGitConfig(app.Log) - app.Gui, err = gui.NewGui(app.Common, config, gitConfig, app.Updater, showRecentRepos, dirName) + app.Gui, err = gui.NewGui(common, config, gitConfig, app.Updater, showRecentRepos, dirName) if err != nil { return app, err } @@ -243,97 +204,13 @@ func (app *App) setupRepo() (bool, error) { } func (app *App) Run(filterPath string) error { - if app.ClientContext == "INTERACTIVE_REBASE" { - return app.Rebase() - } - - if app.ClientContext == "EXIT_IMMEDIATELY" { - os.Exit(0) - } - err := app.Gui.RunAndHandleError(filterPath) 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")), 0o644); 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 { return slices.TryForEach(app.closers, func(closer io.Closer) error { return closer.Close() }) } - -// 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} - - if slices.Contains(knownErrorMessages, errorMessage) { - return errorMessage, true - } - - mappings := []errorMapping{ - { - originalError: "fatal: not a git repository", - newError: app.Tr.NotARepository, - }, - } - - if mapping, ok := slices.Find(mappings, func(mapping errorMapping) bool { - return strings.Contains(errorMessage, mapping.originalError) - }); ok { - 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) - - 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) -} diff --git a/pkg/app/daemon/daemon.go b/pkg/app/daemon/daemon.go new file mode 100644 index 000000000..ea71bb956 --- /dev/null +++ b/pkg/app/daemon/daemon.go @@ -0,0 +1,107 @@ +package daemon + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/env" +) + +// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process. +// We do this when git lets us supply a program to run within a git command. +// For example, if we want to ensure that a git command doesn't hang due to +// waiting for an editor to save a commit message, we can tell git to invoke lazygit +// as the editor via 'GIT_EDITOR=lazygit', and use the env var +// 'LAZYGIT_DAEMON_KIND=EXIT_IMMEDIATELY' to specify that we want to run lazygit +// as a daemon which simply exits immediately. Any additional arguments we want +// to pass to a daemon can be done via other env vars. + +type DaemonKind string + +const ( + InteractiveRebase DaemonKind = "INTERACTIVE_REBASE" + ExitImmediately DaemonKind = "EXIT_IMMEDIATELY" +) + +const ( + DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND" + RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO" +) + +type Daemon interface { + Run() error +} + +func Handle(common *common.Common) { + d := getDaemon(common) + if d == nil { + return + } + + if err := d.Run(); err != nil { + log.Fatal(err) + } + + os.Exit(0) +} + +func InDaemonMode() bool { + return getDaemonKind() != "" +} + +func getDaemon(common *common.Common) Daemon { + switch getDaemonKind() { + case InteractiveRebase: + return &rebaseDaemon{c: common} + case ExitImmediately: + return &exitImmediatelyDaemon{c: common} + } + + return nil +} + +func getDaemonKind() DaemonKind { + return DaemonKind(os.Getenv(DaemonKindEnvKey)) +} + +type rebaseDaemon struct { + c *common.Common +} + +func (self *rebaseDaemon) Run() error { + self.c.Log.Info("Lazygit invoked as interactive rebase demon") + self.c.Log.Info("args: ", os.Args) + + if strings.HasSuffix(os.Args[1], "git-rebase-todo") { + if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); 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 { + self.c.Log.Info("Lazygit demon did not match on any use cases") + } + + return nil +} + +func gitDir() string { + dir := env.GetGitDirEnv() + if dir == "" { + return ".git" + } + return dir +} + +type exitImmediatelyDaemon struct { + c *common.Common +} + +func (self *exitImmediatelyDaemon) Run() error { + return nil +} diff --git a/pkg/app/errors.go b/pkg/app/errors.go new file mode 100644 index 000000000..1556e58fe --- /dev/null +++ b/pkg/app/errors.go @@ -0,0 +1,39 @@ +package app + +import ( + "strings" + + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/i18n" +) + +type errorMapping struct { + originalError string + newError string +} + +// 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 knownError(tr *i18n.TranslationSet, err error) (string, bool) { + errorMessage := err.Error() + + knownErrorMessages := []string{tr.MinGitVersionError} + + if slices.Contains(knownErrorMessages, errorMessage) { + return errorMessage, true + } + + mappings := []errorMapping{ + { + originalError: "fatal: not a git repository", + newError: tr.NotARepository, + }, + } + + if mapping, ok := slices.Find(mappings, func(mapping errorMapping) bool { + return strings.Contains(errorMessage, mapping.originalError) + }); ok { + return mapping.newError, true + } + + return "", false +} diff --git a/pkg/app/logging.go b/pkg/app/logging.go index 7df0bb0c2..db59a15d4 100644 --- a/pkg/app/logging.go +++ b/pkg/app/logging.go @@ -1,31 +1,61 @@ -//go:build !windows -// +build !windows - package app import ( + "io/ioutil" "log" "os" - "github.com/aybabtme/humanlog" - "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/sirupsen/logrus" ) -func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) { - cmd := secureexec.Command("tail", "-f", logFilePath) - - stdout, _ := cmd.StdoutPipe() - if err := cmd.Start(); err != nil { - log.Fatal(err) +func newLogger(config config.AppConfigurer) *logrus.Entry { + var log *logrus.Logger + if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" { + log = newDevelopmentLogger() + } else { + log = newProductionLogger() } - if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil { - log.Fatal(err) - } + // highly recommended: tail -f development.log | humanlog + // https://github.com/aybabtme/humanlog + log.Formatter = &logrus.JSONFormatter{} - if err := cmd.Wait(); err != nil { - log.Fatal(err) - } - - os.Exit(0) + return log.WithFields(logrus.Fields{ + "debug": config.GetDebug(), + "version": config.GetVersion(), + "commit": config.GetCommit(), + "buildDate": config.GetBuildDate(), + }) +} + +func newProductionLogger() *logrus.Logger { + log := logrus.New() + log.Out = ioutil.Discard + log.SetLevel(logrus.ErrorLevel) + return log +} + +func newDevelopmentLogger() *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, 0o666) + if err != nil { + log.Fatalf("Unable to log to log file: %v", err) + } + logger.SetOutput(file) + return logger +} + +func getLogLevel() logrus.Level { + strLevel := os.Getenv("LOG_LEVEL") + level, err := logrus.ParseLevel(strLevel) + if err != nil { + return logrus.DebugLevel + } + return level } diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 5852f127f..5117e6652 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -55,7 +55,11 @@ func generateAtDir(cheatsheetDir string) { for lang := range translationSetsByLang { mConfig.GetUserConfig().Gui.Language = lang - mApp, _ := app.NewApp(mConfig) + common, err := app.NewCommon(mConfig) + if err != nil { + log.Fatal(err) + } + mApp, _ := app.NewApp(mConfig, common) path := cheatsheetDir + "/Keybindings_" + lang + ".md" file, err := os.Create(path) if err != nil { diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index ab7963c44..e69c8b1bd 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -8,6 +8,7 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/app/daemon" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" ) @@ -109,8 +110,8 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo } cmdObj.AddEnvVars( - "LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE", - "LAZYGIT_REBASE_TODO="+todo, + daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase), + daemon.RebaseTODOEnvKey+"="+todo, "DEBUG="+debug, "LANG=en_US.UTF-8", // Force using EN as language "LC_ALL=en_US.UTF-8", // Force using EN as language @@ -297,7 +298,7 @@ func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) erro lazyGitPath := oscommands.GetLazygitPath() return cmdObj. AddEnvVars( - "LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY", + daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately), "GIT_EDITOR="+lazyGitPath, "EDITOR="+lazyGitPath, "VISUAL="+lazyGitPath, diff --git a/pkg/commands/git_commands/rebase_test.go b/pkg/commands/git_commands/rebase_test.go index 4e7b5c2c6..c4d18000f 100644 --- a/pkg/commands/git_commands/rebase_test.go +++ b/pkg/commands/git_commands/rebase_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/app/daemon" "github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" @@ -61,7 +62,7 @@ func TestRebaseSkipEditorCommand(t *testing.T) { `^VISUAL=.*$`, `^EDITOR=.*$`, `^GIT_EDITOR=.*$`, - "^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$", + "^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$", } { regexStr := regexStr foundMatch := lo.ContainsBy(envVars, func(envVar string) bool { diff --git a/pkg/env/env.go b/pkg/env/env.go index 9c0f4816f..8d7993a9a 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -1,6 +1,10 @@ package env -import "os" +import ( + "os" +) + +// This package encapsulates accessing/mutating the ENV of the program. func GetGitDirEnv() string { return os.Getenv("GIT_DIR") diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go new file mode 100644 index 000000000..a4fe94031 --- /dev/null +++ b/pkg/logs/logs.go @@ -0,0 +1,34 @@ +package logs + +import ( + "fmt" + "log" + "os" + + "github.com/aybabtme/humanlog" + "github.com/jesseduffield/lazygit/pkg/config" +) + +// TailLogs lets us run `lazygit --logs` to print the logs produced by other lazygit processes. +// This makes for easier debugging. +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) +} diff --git a/pkg/logs/logs_default.go b/pkg/logs/logs_default.go new file mode 100644 index 000000000..b4474720c --- /dev/null +++ b/pkg/logs/logs_default.go @@ -0,0 +1,31 @@ +//go:build !windows +// +build !windows + +package logs + +import ( + "log" + "os" + + "github.com/aybabtme/humanlog" + "github.com/jesseduffield/lazygit/pkg/secureexec" +) + +func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) { + cmd := secureexec.Command("tail", "-f", logFilePath) + + stdout, _ := cmd.StdoutPipe() + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + + 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) +} diff --git a/pkg/app/logging_windows.go b/pkg/logs/logs_windows.go similarity index 99% rename from pkg/app/logging_windows.go rename to pkg/logs/logs_windows.go index efbdfbbe1..7fa17db26 100644 --- a/pkg/app/logging_windows.go +++ b/pkg/logs/logs_windows.go @@ -1,7 +1,7 @@ //go:build windows // +build windows -package app +package logs import ( "bufio" From 3bf0c9ef441f9343172ed71b5e5947c69e66ac9a Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 7 May 2022 15:42:36 +1000 Subject: [PATCH 2/2] more documentation --- pkg/gui/app_status_manager.go | 14 +++--- pkg/gui/arrangement.go | 3 ++ pkg/gui/confirmation_panel.go | 3 ++ pkg/gui/context.go | 5 ++ pkg/gui/lbl/state.go | 3 ++ pkg/gui/mergeconflicts/state.go | 1 + pkg/gui/popup/fake_popup_handler.go | 51 +++++++++++++++++++ pkg/gui/popup/popup_handler.go | 76 ++++++----------------------- pkg/integration/integration.go | 2 + pkg/tasks/tasks.go | 9 +++- test/lazyintegration/main.go | 2 +- test/runner/main.go | 2 +- 12 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 pkg/gui/popup/fake_popup_handler.go diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go index 34c97a5f4..2c5a8b1ec 100644 --- a/pkg/gui/app_status_manager.go +++ b/pkg/gui/app_status_manager.go @@ -8,18 +8,20 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -type appStatus struct { - message string - statusType string - id int -} - +// statusManager's job is to handle rendering of loading states and toast notifications +// that you see at the bottom left of the screen. type statusManager struct { statuses []appStatus nextId int mutex sync.Mutex } +type appStatus struct { + message string + statusType string + id int +} + func (m *statusManager) removeStatus(id int) { m.mutex.Lock() defer m.mutex.Unlock() diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index de8c9247f..003156dbb 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -7,6 +7,9 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +// In this file we use the boxlayout package, along with knowledge about the app's state, +// to arrange the windows (i.e. panels) on the screen. + const INFO_SECTION_PADDING = " " func (gui *Gui) mainSectionChildren() []*boxlayout.Box { diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 994433a94..e2ea9869f 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -12,6 +12,9 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +// This file is for the rendering of confirmation panels along with setting and handling associated +// keybindings. + func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error { return func() error { if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil { diff --git a/pkg/gui/context.go b/pkg/gui/context.go index 19db274d8..b83aa71c9 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -13,6 +13,11 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/types" ) +// This file is for the management of contexts. There is a context stack such that +// for example you might start off in the commits context and then open a menu, putting +// you in the menu context. When contexts are activated/deactivated certain things need +// to happen like showing/hiding views and rendering content. + func (gui *Gui) popupViewNames() []string { popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool { return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP diff --git a/pkg/gui/lbl/state.go b/pkg/gui/lbl/state.go index 8ff7d7e88..08febbde8 100644 --- a/pkg/gui/lbl/state.go +++ b/pkg/gui/lbl/state.go @@ -5,6 +5,9 @@ import ( "github.com/sirupsen/logrus" ) +// State represents the current state of the line-by-line context i.e. when +// you're staging a file line-by-line or you're building a patch from an existing commit +// this struct holds the info about the diff you're interacting with and what's currently selected. type State struct { selectedLineIdx int rangeStartLineIdx int diff --git a/pkg/gui/mergeconflicts/state.go b/pkg/gui/mergeconflicts/state.go index 41a072617..83969a8eb 100644 --- a/pkg/gui/mergeconflicts/state.go +++ b/pkg/gui/mergeconflicts/state.go @@ -6,6 +6,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +// State represents the selection state of the merge conflict context. type State struct { sync.Mutex diff --git a/pkg/gui/popup/fake_popup_handler.go b/pkg/gui/popup/fake_popup_handler.go new file mode 100644 index 000000000..08edd0e8a --- /dev/null +++ b/pkg/gui/popup/fake_popup_handler.go @@ -0,0 +1,51 @@ +package popup + +import "github.com/jesseduffield/lazygit/pkg/gui/types" + +type FakePopupHandler struct { + OnErrorMsg func(message string) error + OnConfirm func(opts types.ConfirmOpts) error + OnPrompt func(opts types.PromptOpts) error +} + +var _ types.IPopupHandler = &FakePopupHandler{} + +func (self *FakePopupHandler) Error(err error) error { + return self.ErrorMsg(err.Error()) +} + +func (self *FakePopupHandler) ErrorMsg(message string) error { + return self.OnErrorMsg(message) +} + +func (self *FakePopupHandler) Alert(title string, message string) error { + panic("not yet implemented") +} + +func (self *FakePopupHandler) Confirm(opts types.ConfirmOpts) error { + return self.Confirm(opts) +} + +func (self *FakePopupHandler) Prompt(opts types.PromptOpts) error { + return self.OnPrompt(opts) +} + +func (self *FakePopupHandler) WithLoaderPanel(message string, f func() error) error { + return f() +} + +func (self *FakePopupHandler) WithWaitingStatus(message string, f func() error) error { + return f() +} + +func (self *FakePopupHandler) Menu(opts types.CreateMenuOptions) error { + panic("not yet implemented") +} + +func (self *FakePopupHandler) Toast(message string) { + panic("not yet implemented") +} + +func (self *FakePopupHandler) GetPromptInput() string { + panic("not yet implemented") +} diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index 3b715870b..53c52c74e 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -11,7 +11,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -type RealPopupHandler struct { +type PopupHandler struct { *common.Common index int sync.Mutex @@ -24,7 +24,7 @@ type RealPopupHandler struct { getPromptInputFn func() string } -var _ types.IPopupHandler = &RealPopupHandler{} +var _ types.IPopupHandler = &PopupHandler{} func NewPopupHandler( common *common.Common, @@ -35,8 +35,8 @@ func NewPopupHandler( withWaitingStatusFn func(message string, f func() error) error, toastFn func(message string), getPromptInputFn func() string, -) *RealPopupHandler { - return &RealPopupHandler{ +) *PopupHandler { + return &PopupHandler{ Common: common, index: 0, createPopupPanelFn: createPopupPanelFn, @@ -49,19 +49,19 @@ func NewPopupHandler( } } -func (self *RealPopupHandler) Menu(opts types.CreateMenuOptions) error { +func (self *PopupHandler) Menu(opts types.CreateMenuOptions) error { return self.createMenuFn(opts) } -func (self *RealPopupHandler) Toast(message string) { +func (self *PopupHandler) Toast(message string) { self.toastFn(message) } -func (self *RealPopupHandler) WithWaitingStatus(message string, f func() error) error { +func (self *PopupHandler) WithWaitingStatus(message string, f func() error) error { return self.withWaitingStatusFn(message, f) } -func (self *RealPopupHandler) Error(err error) error { +func (self *PopupHandler) Error(err error) error { if err == gocui.ErrQuit { return err } @@ -69,7 +69,7 @@ func (self *RealPopupHandler) Error(err error) error { return self.ErrorMsg(err.Error()) } -func (self *RealPopupHandler) ErrorMsg(message string) error { +func (self *PopupHandler) ErrorMsg(message string) error { self.Lock() self.index++ self.Unlock() @@ -83,11 +83,11 @@ func (self *RealPopupHandler) ErrorMsg(message string) error { return self.Alert(self.Tr.Error, coloredMessage) } -func (self *RealPopupHandler) Alert(title string, message string) error { +func (self *PopupHandler) Alert(title string, message string) error { return self.Confirm(types.ConfirmOpts{Title: title, Prompt: message}) } -func (self *RealPopupHandler) Confirm(opts types.ConfirmOpts) error { +func (self *PopupHandler) Confirm(opts types.ConfirmOpts) error { self.Lock() self.index++ self.Unlock() @@ -101,7 +101,7 @@ func (self *RealPopupHandler) Confirm(opts types.ConfirmOpts) error { }) } -func (self *RealPopupHandler) Prompt(opts types.PromptOpts) error { +func (self *PopupHandler) Prompt(opts types.PromptOpts) error { self.Lock() self.index++ self.Unlock() @@ -117,7 +117,7 @@ func (self *RealPopupHandler) Prompt(opts types.PromptOpts) error { }) } -func (self *RealPopupHandler) WithLoaderPanel(message string, f func() error) error { +func (self *PopupHandler) WithLoaderPanel(message string, f func() error) error { index := 0 self.Lock() self.index++ @@ -150,54 +150,6 @@ func (self *RealPopupHandler) WithLoaderPanel(message string, f func() error) er // returns the content that has currently been typed into the prompt. Useful for // asynchronously updating the suggestions list under the prompt. -func (self *RealPopupHandler) GetPromptInput() string { +func (self *PopupHandler) GetPromptInput() string { return self.getPromptInputFn() } - -type TestPopupHandler struct { - OnErrorMsg func(message string) error - OnConfirm func(opts types.ConfirmOpts) error - OnPrompt func(opts types.PromptOpts) error -} - -var _ types.IPopupHandler = &TestPopupHandler{} - -func (self *TestPopupHandler) Error(err error) error { - return self.ErrorMsg(err.Error()) -} - -func (self *TestPopupHandler) ErrorMsg(message string) error { - return self.OnErrorMsg(message) -} - -func (self *TestPopupHandler) Alert(title string, message string) error { - panic("not yet implemented") -} - -func (self *TestPopupHandler) Confirm(opts types.ConfirmOpts) error { - return self.Confirm(opts) -} - -func (self *TestPopupHandler) Prompt(opts types.PromptOpts) error { - return self.OnPrompt(opts) -} - -func (self *TestPopupHandler) WithLoaderPanel(message string, f func() error) error { - return f() -} - -func (self *TestPopupHandler) WithWaitingStatus(message string, f func() error) error { - return f() -} - -func (self *TestPopupHandler) Menu(opts types.CreateMenuOptions) error { - panic("not yet implemented") -} - -func (self *TestPopupHandler) Toast(message string) { - panic("not yet implemented") -} - -func (self *TestPopupHandler) GetPromptInput() string { - panic("not yet implemented") -} diff --git a/pkg/integration/integration.go b/pkg/integration/integration.go index 4f287ab5f..08b2c8847 100644 --- a/pkg/integration/integration.go +++ b/pkg/integration/integration.go @@ -18,6 +18,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/secureexec" ) +// This package is for running our integration test suite. See docs/Integration_Tests.md for more info + type Test struct { Name string `json:"name"` Speed float64 `json:"speed"` diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index fe257ae96..48d01e5e4 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -14,6 +14,14 @@ import ( "github.com/sirupsen/logrus" ) +// This file revolves around running commands that will be output to the main panel +// in the gui. If we're flicking through the commits panel, we want to invoke a +// `git show` command for each commit, but we don't want to read the entire output +// at once (because that would slow things down); we just want to fill the panel +// and then read more as the user scrolls down. We also want to ensure that we're only +// ever running one `git show` command at time, and that we only have one command +// writing its output to the main panel at a time. + const THROTTLE_TIME = time.Millisecond * 30 // we use this to check if the system is under stress right now. Hopefully this makes sense on other machines @@ -26,7 +34,6 @@ type ViewBufferManager struct { // this is what we write the output of the task to. It's typically a view writer io.Writer - // this is for when we wait to get waitingMutex sync.Mutex taskIDMutex sync.Mutex Log *logrus.Entry diff --git a/test/lazyintegration/main.go b/test/lazyintegration/main.go index 2d8e4a4a9..89d4842b8 100644 --- a/test/lazyintegration/main.go +++ b/test/lazyintegration/main.go @@ -14,7 +14,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/secureexec" ) -// this program lets you manage integration tests in a TUI. +// this program lets you manage integration tests in a TUI. See docs/Integration_Tests.md for more info. type App struct { tests []*integration.Test diff --git a/test/runner/main.go b/test/runner/main.go index 509b66772..58ccd591f 100644 --- a/test/runner/main.go +++ b/test/runner/main.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -// see https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md +// see docs/Integration_Tests.md // This file can be invoked directly, but you might find it easier to go through // test/lazyintegration/main.go, which provides a convenient gui wrapper to integration tests. //