mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 02:24:25 -05:00
A handful of default keybindings differ by platform (e.g. word-wise cursor movement in text inputs uses alt on macOS but ctrl elsewhere). Lazygit chooses these based on the OS it runs on, but that's the wrong signal when the OS isn't where the user is actually typing: someone running lazygit in a Linux container that they access over ssh from a Mac gets the Linux bindings, when they'd rather have the Mac ones. Remapping each binding by hand via config is tedious, so add a single LAZYGIT_KEYBINDING_PLATFORM override. An unrecognized value falls back to the real OS rather than to the non-darwin default bindings, since the latter would be an arbitrary choice.
335 lines
9.8 KiB
Go
335 lines
9.8 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
_ "net/http/pprof"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
|
|
"github.com/integrii/flaggy"
|
|
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
|
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/env"
|
|
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
|
"github.com/jesseduffield/lazygit/pkg/logs/tail"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type cliArgs struct {
|
|
RepoPath string
|
|
FilterPath string
|
|
GitArg string
|
|
UseConfigDir string
|
|
WorkTree string
|
|
GitDir string
|
|
CustomConfigFile string
|
|
ScreenMode string
|
|
PrintVersionInfo bool
|
|
Debug bool
|
|
TailLogs bool
|
|
Profile bool
|
|
PrintDefaultConfig bool
|
|
PrintConfigDir bool
|
|
}
|
|
|
|
type BuildInfo struct {
|
|
Commit string
|
|
Date string
|
|
Version string
|
|
BuildSource string
|
|
}
|
|
|
|
func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTest) {
|
|
cliArgs := parseCliArgsAndEnvVars()
|
|
mergeBuildInfo(buildInfo)
|
|
|
|
if cliArgs.RepoPath != "" {
|
|
if cliArgs.WorkTree != "" || cliArgs.GitDir != "" {
|
|
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
|
}
|
|
|
|
absRepoPath, err := filepath.Abs(cliArgs.RepoPath)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if isRepo, err := isDirectoryAGitRepository(absRepoPath); err != nil || !isRepo {
|
|
log.Fatal(absRepoPath + " is not a valid git repository.")
|
|
}
|
|
|
|
cliArgs.GitDir = filepath.Join(absRepoPath, ".git")
|
|
err = os.Chdir(absRepoPath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to change directory to %s: %v", absRepoPath, err)
|
|
}
|
|
} else if cliArgs.WorkTree != "" {
|
|
env.SetWorkTreeEnv(cliArgs.WorkTree)
|
|
|
|
if err := os.Chdir(cliArgs.WorkTree); err != nil {
|
|
log.Fatalf("Failed to change directory to %s: %v", cliArgs.WorkTree, err)
|
|
}
|
|
}
|
|
|
|
if cliArgs.CustomConfigFile != "" {
|
|
os.Setenv("LG_CONFIG_FILE", cliArgs.CustomConfigFile)
|
|
}
|
|
|
|
if cliArgs.UseConfigDir != "" {
|
|
os.Setenv("CONFIG_DIR", cliArgs.UseConfigDir)
|
|
}
|
|
|
|
if cliArgs.GitDir != "" {
|
|
env.SetGitDirEnv(cliArgs.GitDir)
|
|
}
|
|
|
|
if cliArgs.PrintVersionInfo {
|
|
gitVersion := getGitVersionInfo()
|
|
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s, git version=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH, gitVersion)
|
|
os.Exit(0)
|
|
}
|
|
|
|
if cliArgs.PrintDefaultConfig {
|
|
var buf bytes.Buffer
|
|
encoder := yaml.NewEncoder(&buf)
|
|
err := encoder.Encode(config.GetDefaultConfigForPlatform(config.KeybindingPlatform()))
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
fmt.Printf("%s\n", buf.String())
|
|
os.Exit(0)
|
|
}
|
|
|
|
if cliArgs.PrintConfigDir {
|
|
fmt.Printf("%s\n", config.ConfigDir())
|
|
os.Exit(0)
|
|
}
|
|
|
|
if cliArgs.TailLogs {
|
|
logPath, err := config.LogPath()
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
|
|
tail.TailLogs(logPath)
|
|
os.Exit(0)
|
|
}
|
|
|
|
tempDirBase := getTempDirBase()
|
|
tempDir, err := os.MkdirTemp(tempDirBase, "lazygit-*")
|
|
if err != nil {
|
|
if os.IsPermission(err) {
|
|
log.Fatalf("Your temp directory (%s) is not writeable. Try if rebooting your machine fixes this.", tempDirBase)
|
|
}
|
|
|
|
log.Fatal(err.Error())
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
appConfig, err := config.NewAppConfig("lazygit", buildInfo.Version, buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, cliArgs.Debug, tempDir)
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
|
|
if integrationTest != nil {
|
|
integrationTest.SetupConfig(appConfig)
|
|
// Set this to true so that integration tests don't have to explicitly deal with the hunk
|
|
// staging hint:
|
|
appConfig.GetAppState().DidShowHunkStagingHint = true
|
|
|
|
// Preserve the changes that the test setup just made to the config, so
|
|
// they don't get lost when we reload the config while running the test
|
|
// (which happens when switching between repos, going in and out of
|
|
// submodules, etc).
|
|
appConfig.SaveGlobalUserConfig()
|
|
}
|
|
|
|
common, err := NewCommon(appConfig)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if daemon.InDaemonMode() {
|
|
daemon.Handle(common)
|
|
return
|
|
}
|
|
|
|
if cliArgs.Profile {
|
|
go func() {
|
|
if err := http.ListenAndServe("localhost:6060", nil); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
parsedGitArg := parseGitArg(cliArgs.GitArg)
|
|
|
|
Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, cliArgs.ScreenMode, integrationTest))
|
|
}
|
|
|
|
func parseCliArgsAndEnvVars() *cliArgs {
|
|
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
|
|
|
repoPath := ""
|
|
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
|
|
|
|
filterPath := ""
|
|
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
|
|
|
gitArg := ""
|
|
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
|
|
|
|
printVersionInfo := false
|
|
flaggy.Bool(&printVersionInfo, "v", "version", "Print the current version")
|
|
|
|
debug := false
|
|
flaggy.Bool(&debug, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
|
|
|
|
tailLogs := false
|
|
flaggy.Bool(&tailLogs, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
|
|
|
profile := false
|
|
flaggy.Bool(&profile, "", "profile", "Start the profiler and serve it on http port 6060. See CONTRIBUTING.md for more info.")
|
|
|
|
printDefaultConfig := false
|
|
flaggy.Bool(&printDefaultConfig, "c", "config", "Print the default config")
|
|
|
|
printConfigDir := false
|
|
flaggy.Bool(&printConfigDir, "cd", "print-config-dir", "Print the config directory")
|
|
|
|
useConfigDir := ""
|
|
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
|
|
|
|
workTree := os.Getenv("GIT_WORK_TREE")
|
|
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
|
|
|
|
gitDir := os.Getenv("GIT_DIR")
|
|
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
|
|
|
customConfigFile := ""
|
|
flaggy.String(&customConfigFile, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
|
|
|
|
screenMode := ""
|
|
flaggy.String(&screenMode, "sm", "screen-mode", "The initial screen-mode, which determines the size of the focused panel. Valid options: 'normal' (default), 'half', 'full'")
|
|
|
|
flaggy.Parse()
|
|
|
|
if os.Getenv("DEBUG") == "TRUE" {
|
|
debug = true
|
|
}
|
|
|
|
return &cliArgs{
|
|
RepoPath: repoPath,
|
|
FilterPath: filterPath,
|
|
GitArg: gitArg,
|
|
PrintVersionInfo: printVersionInfo,
|
|
Debug: debug,
|
|
TailLogs: tailLogs,
|
|
Profile: profile,
|
|
PrintDefaultConfig: printDefaultConfig,
|
|
PrintConfigDir: printConfigDir,
|
|
UseConfigDir: useConfigDir,
|
|
WorkTree: workTree,
|
|
GitDir: gitDir,
|
|
CustomConfigFile: customConfigFile,
|
|
ScreenMode: screenMode,
|
|
}
|
|
}
|
|
|
|
func parseGitArg(gitArg string) appTypes.GitArg {
|
|
typedArg := appTypes.GitArg(gitArg)
|
|
|
|
// using switch so that linter catches when a new git arg value is defined but not handled here
|
|
switch typedArg {
|
|
case appTypes.GitArgNone, appTypes.GitArgStatus, appTypes.GitArgBranch, appTypes.GitArgLog, appTypes.GitArgStash:
|
|
return typedArg
|
|
}
|
|
|
|
permittedValues := []string{
|
|
string(appTypes.GitArgStatus),
|
|
string(appTypes.GitArgBranch),
|
|
string(appTypes.GitArgLog),
|
|
string(appTypes.GitArgStash),
|
|
}
|
|
|
|
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",
|
|
gitArg,
|
|
strings.Join(permittedValues, ", "),
|
|
)
|
|
|
|
panic("unreachable")
|
|
}
|
|
|
|
// the buildInfo struct we get passed in is based on what's baked into the lazygit
|
|
// binary via the LDFLAGS argument. Some lazygit distributions will make use of these
|
|
// arguments and some will not. Go recently started baking in build info
|
|
// into the binary by default e.g. the git commit hash. So in this function
|
|
// we merge the two together, giving priority to the stuff set by LDFLAGS.
|
|
// Note: this mutates the argument passed in
|
|
func mergeBuildInfo(buildInfo *BuildInfo) {
|
|
// if the version has already been set by build flags then we'll honour that.
|
|
// chances are it's something like v0.31.0 which is more informative than a
|
|
// commit hash.
|
|
if buildInfo.Version != "" {
|
|
return
|
|
}
|
|
|
|
buildInfo.Version = "unversioned"
|
|
|
|
goBuildInfo, ok := debug.ReadBuildInfo()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
revision, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
|
return setting.Key == "vcs.revision"
|
|
})
|
|
if ok {
|
|
buildInfo.Commit = revision.Value
|
|
// if lazygit was built from source we'll show the version as the
|
|
// abbreviated commit hash
|
|
buildInfo.Version = utils.ShortHash(revision.Value)
|
|
}
|
|
|
|
// if version hasn't been set we assume that neither has the date
|
|
time, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
|
return setting.Key == "vcs.time"
|
|
})
|
|
if ok {
|
|
buildInfo.Date = time.Value
|
|
}
|
|
}
|
|
|
|
func getGitVersionInfo() string {
|
|
cmd := exec.Command("git", "--version")
|
|
stdout, _ := cmd.Output()
|
|
gitVersion := strings.Trim(strings.TrimPrefix(string(stdout), "git version "), " \r\n")
|
|
return gitVersion
|
|
}
|
|
|
|
func getTempDirBase() string {
|
|
tempDir := os.TempDir()
|
|
|
|
user, err := user.Current()
|
|
if err != nil || user.Uid == "" {
|
|
return tempDir
|
|
}
|
|
|
|
tmpDirBase := filepath.Join(tempDir, "lazygit-"+user.Uid)
|
|
if err := os.MkdirAll(tmpDirBase, 0o700); err != nil {
|
|
return tempDir
|
|
}
|
|
|
|
return tmpDirBase
|
|
}
|