package daemon import ( "fmt" "io" "log" "os" "path/filepath" "strings" "github.com/fsmiamoto/git-todo-parser/todo" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/utils" ) // 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" // The `PrependLinesEnvKey` env variable is set to `true` to tell our daemon // to prepend the content of `RebaseTODOEnvKey` to the default `git-rebase-todo` // file instead of using it as a replacement. PrependLinesEnvKey string = "LAZYGIT_PREPEND_LINES" // If this is set, it tells lazygit to read the original todo file, and // change the action for one or more entries in it. The value of the variable // will have one or more lines of the form "Sha1:newAction", e.g. // a02b54e1b7e7e8dd8bc1958c11ef4ee4df459ea4:edit // The existing action of the todo to be changed is expected to be "pick". // // If this is used, the value of RebaseTODOEnvKey must be empty. ChangeTodoActionEnvKey string = "LAZYGIT_CHANGE_TODO_ACTION" // Can be set to the sha of a "pick" todo that will be moved down by one. MoveTodoDownEnvKey string = "LAZYGIT_MOVE_COMMIT_DOWN" // Can be set to the sha of a "pick" todo that will be moved up by one. MoveTodoUpEnvKey string = "LAZYGIT_MOVE_COMMIT_UP" ) type Daemon interface { Run() error } var logFile io.StringWriter func Handle(common *common.Common) { logFile, _ = os.Create("/tmp/daemon-log.txt") _, _ = logFile.WriteString("Hello Daemon\n") 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) path := os.Args[1] if strings.HasSuffix(path, "git-rebase-todo") { return self.writeTodoFile(path) } else if strings.HasSuffix(path, 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 (self *rebaseDaemon) writeTodoFile(path string) error { if changeTodoActionEnvValue := os.Getenv(ChangeTodoActionEnvKey); changeTodoActionEnvValue != "" { return self.changeTodoAction(path, changeTodoActionEnvValue) } else if shaToMoveDown := os.Getenv(MoveTodoDownEnvKey); shaToMoveDown != "" { _, _ = logFile.WriteString(fmt.Sprintf("Moving commit down: %s\n", shaToMoveDown)) return utils.MoveTodoDown(path, shaToMoveDown, todo.Pick) } else if shaToMoveUp := os.Getenv(MoveTodoUpEnvKey); shaToMoveUp != "" { _, _ = logFile.WriteString(fmt.Sprintf("Moving commit up: %s\n", shaToMoveUp)) return utils.MoveTodoUp(path, shaToMoveUp, todo.Pick) } else { todoContent := []byte(os.Getenv(RebaseTODOEnvKey)) prependLines := os.Getenv(PrependLinesEnvKey) != "" if prependLines { existingContent, err := os.ReadFile(path) if err != nil { return err } todoContent = append(todoContent, existingContent...) } return os.WriteFile(path, todoContent, 0o644) } } func (self *rebaseDaemon) changeTodoAction(path string, changeTodoActionEnvValue string) error { lines := strings.Split(changeTodoActionEnvValue, "\n") for _, line := range lines { fields := strings.Split(line, ":") if len(fields) != 2 { return fmt.Errorf("Unexpected value for %s: %s", ChangeTodoActionEnvKey, changeTodoActionEnvValue) } sha, newAction := fields[0], self.actionFromString(fields[1]) if int(newAction) == 0 { return fmt.Errorf("Unknown action in %s", changeTodoActionEnvValue) } if err := utils.EditRebaseTodo(path, sha, todo.Pick, newAction); err != nil { return err } } return nil } func (self *rebaseDaemon) actionFromString(actionString string) todo.TodoCommand { for t := todo.Pick; t < todo.Comment; t++ { if t.String() == actionString { return t } } return 0 } 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 }