package cmd import ( "context" "os" "os/signal" "path/filepath" "regexp" "strings" "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/alias" "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/cache" conf "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/config" "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/project" "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/config" ) var rootCmd = &cobra.Command{ Use: "git-project-manager", Aliases: []string{"gpm"}, Short: "Find and use Git projects locally", Long: util.RootCmdLong, PersistentPreRun: initRootCmd, } var ( Version = "development" configExemptCommands = regexp.MustCompile(`^(doc|conf)`) utils *util.Utils ) // Hook traversal is enabled, so this will be run for all // sub-commands regardless of their registered pre-hooks func initRootCmd(cmd *cobra.Command, args []string) { cmd.SetContext(util.AddToCtx(cmd.Context(), utils)) utils.InitProjectPath(cmd, args) } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { ctx, cncl := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) defer cncl() err := rootCmd.ExecuteContext(ctx) if err != nil { pterm.Error.Printfln("%s", pterm.LightYellow("Command failed, "+err.Error())) os.Exit(1) } } func init() { cobra.EnableTraverseRunHooks = true utils = &util.Utils{} cobra.OnInitialize(getInitConfigFunc(utils)) // Global flags rootCmd.PersistentFlags().String(util.FlagConfig, "", "config file (default is "+util.DefConfigPath+")") rootCmd.PersistentFlags().String(util.FlagPath, "", "Sets a path for local clones of projects") rootCmd.PersistentFlags().String(util.FlagLogLevel, util.DefLogLevel, "Default log level -- info, warn, error, debug") rootCmd.PersistentFlags().StringSlice(util.FlagRemote, []string{}, "Specify remotes by host for any sub-command. Provide multiple times or comma delimited.") viper.BindPFlags(rootCmd.PersistentFlags()) // Flag autocompletion rootCmd.RegisterFlagCompletionFunc(util.FlagLogLevel, util.ValidLogLevelsFunc) rootCmd.RegisterFlagCompletionFunc(util.FlagRemote, util.ValidRemotesFunc) // Subcommands rootCmd.AddCommand(alias.AliasCmd) rootCmd.AddCommand(cache.CacheCmd) rootCmd.AddCommand(conf.ConfigCmd) rootCmd.AddCommand(project.ProjectCmd) // Version rootCmd.Version = Version } // initConfig reads in config file and ENV variables if set. func getInitConfigFunc(utils *util.Utils) func() { return func() { cfgFile := viper.GetString(util.FlagConfig) if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) } else { // Find home directory. home, err := os.UserHomeDir() cobra.CheckErr(err) // Search config in home directory with name ".git-project-manager" (without extension). configPath := filepath.Join(home, ".config") viper.AddConfigPath(configPath) viper.SetConfigType("yaml") viper.SetConfigName(util.GetConfigName(configPath)) } viper.AutomaticEnv() viper.ReadInConfig() // Configure pretty logger plog := pterm.DefaultLogger. WithLevel(getPtermLogLevel(viper.GetString(util.FlagLogLevel))). WithWriter(os.Stderr) if plog.Level == pterm.LogLevelDebug { pterm.EnableDebugMessages() } utils.SetLogger(plog) // Load into struct to not be so darn pythonic, retrieving // settings by untyped string "name" conf := new(config.Config) if err := viper.Unmarshal(&conf); err != nil { plog.Error("Failed loading config", plog.Args("err", err)) } utils.SetConfig(conf) if len(os.Args) > 0 && configExemptCommands.Match([]byte(os.Args[1])) { plog.Debug("Permitting missing config for config sub-command") return } else if conf.ProjectPath == "" { plog.Fatal("Minimal configuration missing, must have projectPath", plog.Args( "do", "Try running `git-project-manager config default > "+util.DefConfigPath, )) } checkConfigPerms(viper.ConfigFileUsed()) // Abort on world-readable config utils.Logger().Debug("Configuration loaded", plog.Args("conf", conf)) } } func getPtermLogLevel(level string) pterm.LogLevel { var pLevel pterm.LogLevel switch strings.ToLower(level) { case "error": pLevel = pterm.LogLevelError case "warn": pLevel = pterm.LogLevelWarn case "info": pLevel = pterm.LogLevelInfo case "debug": pLevel = pterm.LogLevelDebug default: pLevel = pterm.LogLevelInfo } return pLevel } // Don't allow world-readable configuration func checkConfigPerms(file string) { stat, err := os.Stat(file) if err != nil { utils.Logger().Error("Failure reading configuration", utils.Logger().Args("err", err)) return } if stat.Mode().Perm()&0o004 == 0o004 { utils.Logger().Error("Configuration is world-readable. Recomment 0400.", utils.Logger().Args("mode", stat.Mode().String())) os.Exit(1) } }