diff --git a/cmd/alias.go b/cmd/alias.go deleted file mode 100644 index 0021967..0000000 --- a/cmd/alias.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -var aliasCmd = &cobra.Command{ - Use: "alias", - Aliases: []string{"aliases", "a"}, - Short: "Manage project aliases", - Long: aliasCmdLong, - // Just re-use the hooks for project - PersistentPreRun: initProjectCmd, - PersistentPostRun: postProjectCmd, -} - -func init() { - rootCmd.AddCommand(aliasCmd) -} - -func mustHaveAliases(cmd *cobra.Command, args []string) { - if len(projectCache.Aliases) == 0 { - plog.Fatal("No aliases set, nothing to " + cmd.Name()) - } -} diff --git a/cmd/alias/alias.go b/cmd/alias/alias.go new file mode 100644 index 0000000..f9feef7 --- /dev/null +++ b/cmd/alias/alias.go @@ -0,0 +1,37 @@ +package alias + +import ( + "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" + "github.com/spf13/cobra" +) + +var AliasCmd = &cobra.Command{ + Use: "alias", + Aliases: []string{"aliases", "a"}, + Short: "Manage project aliases", + Long: util.AliasCmdLong, + // Just re-use the hooks for project + PersistentPreRun: util.InitProjects, + PersistentPostRun: util.PostProjectCmd, +} + +var utils *util.Utils + +func aliasCmdPreRun(cmd *cobra.Command, args []string) { + utils = util.MustFromCtx(cmd.Context()) + util.InitProjects(cmd, args) +} + +func mustHaveAliases(cmd *cobra.Command, args []string) { + utils := util.MustFromCtx(cmd.Context()) + + if len(utils.Cache().Aliases) == 0 { + utils.Logger().Fatal("No aliases set, nothing to " + cmd.Name()) + } +} + +func init() { + AliasCmd.AddCommand(aliasAddCmd) + AliasCmd.AddCommand(aliasListCmd) + AliasCmd.AddCommand(aliasDeleteCmd) +} diff --git a/cmd/alias_add.go b/cmd/alias/alias_add.go similarity index 53% rename from cmd/alias_add.go rename to cmd/alias/alias_add.go index 3139136..2d543ef 100644 --- a/cmd/alias_add.go +++ b/cmd/alias/alias_add.go @@ -1,4 +1,4 @@ -package cmd +package alias import ( "fmt" @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" ) @@ -17,7 +18,7 @@ var aliasAddCmd = &cobra.Command{ Aliases: []string{"set", "a", "s"}, Short: "Add a project alias", Args: cobra.ArbitraryArgs, - Long: aliasAddCmdLong, + Long: util.AliasAddCmdLong, Run: runAddAliasCmd, } @@ -25,22 +26,22 @@ func runAddAliasCmd(cmd *cobra.Command, args []string) { var project *projects.Project // Check by flag - if projectID := viper.GetInt(ViperAliasAddPID); projectID > 0 { - plog.Debug(fmt.Sprintf("Adding for inbound project ID %d", projectID)) - project = projectCache.GetProjectByID(projectID) + if projectID := viper.GetInt(util.ViperAliasAddPID); projectID > 0 { + utils.Logger().Debug(fmt.Sprintf("Adding for inbound project ID %d", projectID)) + project = utils.Cache().GetProjectByID(projectID) } // Check by arg if len(args) > 0 { - project = fzfFindProject(&fzfProjectOpts{Ctx: cmd.Context(), Search: searchStringFromArgs(args)}) + project = utils.FzfFindProject(&util.FzfProjectOpts{Ctx: cmd.Context(), Search: utils.SearchStringFromArgs(args)}) } // Collect by fzf if project == nil { var err error - project, err = fzfProject(&fzfProjectOpts{Ctx: cmd.Context()}) + project, err = utils.FzfProject(&util.FzfProjectOpts{Ctx: cmd.Context()}) if err != nil || project == nil { - plog.Fatal("No project to alias, nothing to do", plog.Args("error", err)) + utils.Logger().Fatal("No project to alias, nothing to do", utils.Logger().Args("error", err)) } } @@ -48,9 +49,9 @@ func runAddAliasCmd(cmd *cobra.Command, args []string) { } func addNewAliases(projectID int) { - project := projectCache.GetProjectByID(projectID) + project := utils.Cache().GetProjectByID(projectID) if project == nil { - plog.Error("Failed to find project to alias", plog.Args("projectID", projectID)) + utils.Logger().Error("Failed to find project to alias", utils.Logger().Args("projectID", projectID)) return } @@ -63,13 +64,13 @@ func addNewAliases(projectID int) { if a == "" { continue } - if err := projectCache.AddAlias(a, project.ID, project.Remote); err != nil { - plog.Debug("Skipping alias add", plog.Args( + if err := utils.Cache().AddAlias(a, project.ID, project.Remote); err != nil { + utils.Logger().Debug("Skipping alias add", utils.Logger().Args( "error", err, "alias", a, )) } else { - plog.Info("Successfully added alias to project", plog.Args( + utils.Logger().Info("Successfully added alias to project", utils.Logger().Args( "project", project.String(), "alias", a, )) @@ -78,9 +79,9 @@ func addNewAliases(projectID int) { } func promptAliasesForProject(p *projects.Project) []string { - aliases := projectCache.GetProjectAliases(p) + aliases := utils.Cache().GetProjectAliases(p) if len(aliases) > 0 { - plog.Info("Adding aliases to project", plog.Args( + utils.Logger().Info("Adding aliases to project", utils.Logger().Args( "project", p.String(), "existingAliases", cache.ProjectAliasesString(aliases), )) @@ -97,10 +98,9 @@ func promptAliasesForProject(p *projects.Project) []string { } func init() { - aliasCmd.AddCommand(aliasAddCmd) - aliasAddCmd.PersistentFlags().Int(FlagProjectID, 0, "Specify a project by ID") + aliasAddCmd.PersistentFlags().Int(util.FlagProjectID, 0, "Specify a project by ID") - aliasAddCmd.RegisterFlagCompletionFunc(FlagProjectID, validProjectIdFunc) + aliasAddCmd.RegisterFlagCompletionFunc(util.FlagProjectID, util.ValidProjectIdFunc) - viper.BindPFlag(ViperAliasAddPID, aliasAddCmd.Flag(FlagProjectID)) + viper.BindPFlag(util.ViperAliasAddPID, aliasAddCmd.Flag(util.FlagProjectID)) } diff --git a/cmd/alias_delete.go b/cmd/alias/alias_delete.go similarity index 61% rename from cmd/alias_delete.go rename to cmd/alias/alias_delete.go index d53f481..ca8c27e 100644 --- a/cmd/alias_delete.go +++ b/cmd/alias/alias_delete.go @@ -1,19 +1,20 @@ -package cmd +package alias import ( "fmt" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" - "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" ) var aliasDeleteCmd = &cobra.Command{ Use: "delete [fuzzy project or alias]", Aliases: []string{"rm", "del", "d"}, Short: "Delete a project alias", - Long: aliasDeleteCmdLong, + Long: util.AliasDeleteCmdLong, PreRun: mustHaveAliases, Run: runDeleteAliasCmd, } @@ -22,32 +23,32 @@ func runDeleteAliasCmd(cmd *cobra.Command, args []string) { var project *projects.Project var err error - fzfOpts := &fzfProjectOpts{ + fzfOpts := &util.FzfProjectOpts{ Ctx: cmd.Context(), MustHaveAlias: true, } if len(args) > 0 { - fzfOpts.Search = searchStringFromArgs(args) - project = fzfFindProject(fzfOpts) + fzfOpts.Search = utils.SearchStringFromArgs(args) + project = utils.FzfFindProject(fzfOpts) } else { - project, err = fzfProject(fzfOpts) + project, err = utils.FzfProject(fzfOpts) } if project == nil || err != nil { - plog.Fatal("Failed to find project to delete aliases from", plog.Args( + utils.Logger().Fatal("Failed to find project to delete aliases from", utils.Logger().Args( "error", err, )) } - aliasStrings := projectCache.GetProjectAliasStrings(project) + aliasStrings := utils.Cache().GetProjectAliasStrings(project) deletionCandidates, err := pterm.DefaultInteractiveMultiselect. WithOptions(aliasStrings). Show() if err != nil || len(deletionCandidates) < 1 { - plog.Fatal("Failed to find project to delete aliases from", plog.Args( + utils.Logger().Fatal("Failed to find project to delete aliases from", utils.Logger().Args( "error", err, )) } @@ -61,26 +62,25 @@ func runDeleteAliasCmd(cmd *cobra.Command, args []string) { Show() if !confirm { - plog.Warn("Alias deletion cancelled") + utils.Logger().Warn("Alias deletion cancelled") continue } - plog.Info("Deleting alias", plog.Args( + utils.Logger().Info("Deleting alias", utils.Logger().Args( "project", project.String(), "alias", a, )) - projectCache.DeleteAlias(projectCache.GetAliasByName(a)) + utils.Cache().DeleteAlias(utils.Cache().GetAliasByName(a)) } - fmt.Println(projectCache.ProjectString(project)) + fmt.Println(utils.Cache().ProjectString(project)) } func init() { - aliasCmd.AddCommand(aliasDeleteCmd) aliasDeleteCmd.PersistentFlags().Int("projectID", 0, "Specify a project by ID") - aliasDeleteCmd.RegisterFlagCompletionFunc("projectID", validProjectIdFunc) + aliasDeleteCmd.RegisterFlagCompletionFunc("projectID", util.ValidProjectIdFunc) viper.BindPFlag("alias.delete.projectID", aliasDeleteCmd.Flag("projectID")) } diff --git a/cmd/alias_list.go b/cmd/alias/alias_list.go similarity index 86% rename from cmd/alias_list.go rename to cmd/alias/alias_list.go index 9588746..412c49a 100644 --- a/cmd/alias_list.go +++ b/cmd/alias/alias_list.go @@ -1,8 +1,9 @@ -package cmd +package alias import ( "fmt" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -14,7 +15,7 @@ var aliasListCmd = &cobra.Command{ Aliases: []string{"dump", "show", "ls", "ll", "l"}, Short: "List Aliases", PreRun: mustHaveAliases, - Long: aliasListCmdLong, + Long: util.AliasListCmdLong, Run: runListAliasCmd, } diff --git a/cmd/cache.go b/cmd/cache/cache.go similarity index 85% rename from cmd/cache.go rename to cmd/cache/cache.go index c0119f1..60b73cb 100644 --- a/cmd/cache.go +++ b/cmd/cache/cache.go @@ -5,12 +5,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - - "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache" ) -var projectCache *cache.Cache - var cacheCmd = &cobra.Command{ Use: "cache", Aliases: []string{"a", "ln"}, diff --git a/cmd/cache_clear.go b/cmd/cache/cache_clear.go similarity index 100% rename from cmd/cache_clear.go rename to cmd/cache/cache_clear.go diff --git a/cmd/cache_dump.go b/cmd/cache/cache_dump.go similarity index 100% rename from cmd/cache_dump.go rename to cmd/cache/cache_dump.go diff --git a/cmd/cache_load.go b/cmd/cache/cache_load.go similarity index 100% rename from cmd/cache_load.go rename to cmd/cache/cache_load.go diff --git a/cmd/cache_unlock.go b/cmd/cache/cache_unlock.go similarity index 100% rename from cmd/cache_unlock.go rename to cmd/cache/cache_unlock.go diff --git a/cmd/project.go b/cmd/project.go index 7724603..dbd247f 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" ) @@ -15,16 +16,16 @@ var projectCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), ArgAliases: []string{"alias"}, Long: projCmdLong, - PersistentPreRun: initProjectCmd, + PersistentPreRun: initProjects, PersistentPostRun: postProjectCmd, // Run: projectGoCmdRun, } func getProject(args []string) *projects.Project { - remotes := viper.GetStringSlice(FlagRemote) - fzfOpts := &fzfProjectOpts{ + remotes := viper.GetStringSlice(util.FlagRemote) + fzfOpts := &util.FzfProjectOpts{ Ctx: rootCmd.Context(), - Search: searchStringFromArgs(args), + Search: utils.SearchStringFromArgs(args), Remotes: remotes, } project := fzfFindProject(fzfOpts) @@ -47,21 +48,7 @@ func getProject(args []string) *projects.Project { return project } -func initProjectCmd(cmd *cobra.Command, args []string) { - initProjectCache(cmd, args) - mustHaveProjects(cmd, args) -} - -func postProjectCmd(cmd *cobra.Command, args []string) { - postProjectCache(cmd, args) -} - func init() { rootCmd.AddCommand(projectCmd) } -func mustHaveProjects(cmd *cobra.Command, _ []string) { - if len(projectCache.Projects) == 0 { - plog.Fatal("No projects to " + cmd.Name() + ", try running cache load") - } -} diff --git a/cmd/root.go b/cmd/root.go index 60fac1f..62f572a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,28 +12,27 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/config" ) -var ( - conf config.Config - plog *pterm.Logger -) - var rootCmd = &cobra.Command{ Use: "git-project-manager", Aliases: []string{"gpm"}, Short: "Find and use Git projects locally", - Long: rootCmdLong, + Long: util.RootCmdLong, PersistentPreRun: initRootCmd, } -var configExemptCommands = regexp.MustCompile(`^(doc|conf)`) +var ( + utils *util.Utils + configExemptCommands = regexp.MustCompile(`^(doc|conf)`) +) // 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) { - initProjectPath(cmd, args) + utils.InitProjectPath(cmd, args) } // Execute adds all child commands to the root command and sets flags appropriately. @@ -42,6 +41,9 @@ func Execute() { ctx, cncl := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) defer cncl() + // Store provided util.Utils in context + ctx = util.AddToCtx(ctx, utils) + err := rootCmd.ExecuteContext(ctx) if err != nil { pterm.Error.Printfln("%s", pterm.LightYellow("Command failed, "+err.Error())) @@ -51,73 +53,79 @@ func Execute() { func init() { cobra.EnableTraverseRunHooks = true - cobra.OnInitialize(initConfig) + + utils = &util.Utils{} + cobra.OnInitialize(getInitConfigFunc(utils)) // Global flags - rootCmd.PersistentFlags().String(FlagConfig, "", - "config file (default is "+defConfigPath+")") - rootCmd.PersistentFlags().String(FlagPath, "", + 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(FlagLogLevel, defLogLevel, + rootCmd.PersistentFlags().String(util.FlagLogLevel, util.DefLogLevel, "Default log level -- info, warn, error, debug") - rootCmd.PersistentFlags().StringSlice(FlagRemote, []string{}, + rootCmd.PersistentFlags().StringSlice(util.FlagRemote, []string{}, "Specify remotes by host for any sub-command. Provide multiple times or comma delimited.") // Flag autocompletion - rootCmd.RegisterFlagCompletionFunc(FlagLogLevel, validLogLevelsFunc) - rootCmd.RegisterFlagCompletionFunc(FlagRemote, validRemotesFunc) + rootCmd.RegisterFlagCompletionFunc(util.FlagLogLevel, util.ValidLogLevelsFunc) + rootCmd.RegisterFlagCompletionFunc(util.FlagRemote, utils.ValidRemotesFunc) viper.BindPFlags(rootCmd.PersistentFlags()) } // initConfig reads in config file and ENV variables if set. -func initConfig() { - cfgFile := viper.GetString(FlagConfig) - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := os.UserHomeDir() - cobra.CheckErr(err) +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(getConfigName(configPath)) + // 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)) + } + + 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)) } - - viper.AutomaticEnv() - viper.ReadInConfig() - - // Configure pretty logger - plog = pterm.DefaultLogger. - WithLevel(getPtermLogLevel(viper.GetString(FlagLogLevel))). - WithWriter(os.Stderr) - if plog.Level == pterm.LogLevelDebug { - pterm.EnableDebugMessages() - } - - // Load into struct to not be so darn pythonic, retrieving - // settings by untyped string "name" - if err := viper.Unmarshal(&conf); err != nil { - plog.Error("Failed loading config", plog.Args("err", err)) - } - - 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 > "+defConfigPath, - )) - } - - checkConfigPerms(viper.ConfigFileUsed()) // Abort on world-readable config - - plog.Debug("Configuration loaded", plog.Args("conf", conf)) } func getPtermLogLevel(level string) pterm.LogLevel { @@ -141,12 +149,12 @@ func getPtermLogLevel(level string) pterm.LogLevel { func checkConfigPerms(file string) { stat, err := os.Stat(file) if err != nil { - plog.Error("Failure reading configuration", plog.Args("err", err)) + utils.Logger().Error("Failure reading configuration", utils.Logger().Args("err", err)) return } if stat.Mode().Perm()&0o004 == 0o004 { - plog.Error("Configuration is world-readable. Recomment 0400.", - plog.Args("mode", stat.Mode().String())) + utils.Logger().Error("Configuration is world-readable. Recomment 0400.", + utils.Logger().Args("mode", stat.Mode().String())) os.Exit(1) } } diff --git a/cmd/util/util.go b/cmd/util/util.go new file mode 100644 index 0000000..b53e0b8 --- /dev/null +++ b/cmd/util/util.go @@ -0,0 +1,121 @@ +// Common utilities used by various subcommands +package util + +import ( + "context" + "errors" + "sync" + + "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/config" + "github.com/pterm/pterm" +) + +type Utils struct { + ctx context.Context + config *config.Config + logger *pterm.Logger + cache *cache.Cache + sync.RWMutex +} + +type UtilOpts struct { + Ctx context.Context + Config *config.Config + Logger *pterm.Logger + Cache *cache.Cache +} + +type utilsCtxKey uint8 + +const UtilsKey utilsCtxKey = iota + +func AddToCtx(ctx context.Context, u *Utils) context.Context { + return context.WithValue(ctx, UtilsKey, u) +} + +func MustFromCtx(ctx context.Context) *Utils { + utils, err := FromCtx(ctx) + if err != nil { + panic(err) + } + return utils +} + +func FromCtx(ctx context.Context) (*Utils, error) { + utils, avail := ctx.Value(UtilsKey).(*Utils) + if !avail { + return nil, errors.New("invalid util in context") + } else if utils == nil { + return nil, errors.New("util never set in context") + } + + return utils, nil +} + +func (u *Utils) Init(opts *UtilOpts) { + u.ctx = opts.Ctx + u.SetConfig(opts.Config) + u.SetLogger(opts.Logger) + u.SetCache(opts.Cache) +} + +func (u *Utils) SetCache(c *cache.Cache) { + if c == nil { + return + } + + u.Lock() + defer u.Unlock() + u.cache = c +} + +func (u *Utils) Cache() *cache.Cache { + u.RLock() + defer u.RUnlock() + return u.cache +} + +func (u *Utils) SetLogger(l *pterm.Logger) { + if l == nil { + return + } + + u.Lock() + defer u.Unlock() + u.logger = l +} + +func (u *Utils) Logger() *pterm.Logger { + u.RLock() + defer u.RUnlock() + return u.logger +} + +func (u *Utils) SetConfig(conf *config.Config) { + if conf == nil { + return + } + + u.Lock() + defer u.Unlock() + u.config = conf +} + +func (u *Utils) Config() *config.Config { + u.RLock() + defer u.RUnlock() + return u.config +} + +func (u *Utils) SetContext(c context.Context) { + u.Lock() + defer u.Unlock() + u.ctx = c +} + +func (u *Utils) Context() context.Context { + u.RLock() + defer u.RUnlock() + return u.ctx +} diff --git a/cmd/util/util_completion.go b/cmd/util/util_completion.go new file mode 100644 index 0000000..2347e40 --- /dev/null +++ b/cmd/util/util_completion.go @@ -0,0 +1,71 @@ +package util + +import ( + "strconv" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/exp/slices" +) + +func (u *Utils) ValidProjectsFunc(cmd *cobra.Command, args []string, toComplete string) ( + []string, cobra.ShellCompDirective, +) { + u.InitProjectCache(cmd, args) + return u.Cache().ProjectStrings(toComplete), cobra.ShellCompDirectiveNoFileComp +} + +func (u *Utils) ValidAliasesFunc(cmd *cobra.Command, args []string, toComplete string) ( + []string, cobra.ShellCompDirective, +) { + u.InitProjectCache(cmd, args) + return u.Cache().AliasStrings(toComplete), cobra.ShellCompDirectiveNoFileComp +} + +func (u *Utils) ValidProjectsOrAliasesFunc(cmd *cobra.Command, args []string, toComplete string) ( + []string, cobra.ShellCompDirective, +) { + projectStrings, _ := u.ValidAliasesFunc(cmd, args, toComplete) + aliasStrings, _ := u.ValidProjectsFunc(cmd, args, toComplete) + return append(projectStrings, aliasStrings...), cobra.ShellCompDirectiveDefault +} + +func (u *Utils) ValidRemotesFunc(_ *cobra.Command, _ []string, toComplete string) ( + []string, cobra.ShellCompDirective, +) { + remotes := make([]string, 0, len(u.Config().Remotes)) + for _, remote := range u.Config().Remotes { + if strings.HasPrefix(remote.Host, toComplete) { + remotes = append(remotes, remote.Host) + } + } + return remotes, cobra.ShellCompDirectiveNoFileComp +} + +func ValidLogLevelsFunc(_ *cobra.Command, _ []string, toComplete string) ( + []string, cobra.ShellCompDirective, +) { + levels := []string{"info", "warn", "error", "debug"} + matchingLevels := make([]string, 0, len(levels)) + for _, level := range levels { + if strings.HasPrefix(level, toComplete) { + matchingLevels = append(matchingLevels, level) + } + } + return matchingLevels, cobra.ShellCompDirectiveNoFileComp +} + +func ValidProjectIdFunc(cmd *cobra.Command, args []string, toComplete string) ( + []string, cobra.ShellCompDirective, +) { + u := MustFromCtx(cmd.Context()) + u.InitProjectCache(cmd, args) + matchingIds := make([]string, 0, len(u.Cache().Projects)) + for _, p := range u.Cache().Projects { + idString := strconv.FormatInt(int64(p.ID), 10) + if strings.HasPrefix(idString, toComplete) { + matchingIds = append(matchingIds, idString) + } + } + return slices.Clip(matchingIds), cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/util_constants.go b/cmd/util/util_constants.go similarity index 61% rename from cmd/util_constants.go rename to cmd/util/util_constants.go index c269bfe..b6d0d77 100644 --- a/cmd/util_constants.go +++ b/cmd/util/util_constants.go @@ -1,4 +1,4 @@ -package cmd +package util const ( // Cobra Flags @@ -25,62 +25,62 @@ const ( ) const ( - defGitlabHost = "https://gitlab.com" - defLogLevel = "info" - defConfigPath = "~/.config/git-project-manager.yaml" - configName = "git-project-manager" - legacyConfigName = "gitlab-project-manager" + DefGitlabHost = "https://gitlab.com" + DefLogLevel = "info" + DefConfigPath = "~/.config/git-project-manager.yaml" + ConfigName = "git-project-manager" + LegacyConfigName = "gitlab-project-manager" ) -const aliasCmdLong = `Manages project aliases, with options for +const AliasCmdLong = `Manages project aliases, with options for listing, adding, and deleting.` -const aliasListCmdLong = `Lists all aliases by project` +const AliasListCmdLong = `Lists all aliases by project` -const aliasAddCmdLong = `Adds a project alias to a project +const AliasAddCmdLong = `Adds a project alias to a project project ID can be provided, or will otherwise use fuzzy find` -const aliasDeleteCmdLong = `Deletes aliases from projects +const AliasDeleteCmdLong = `Deletes aliases from projects project ID can be provided, or will otherwise use fuzzy find` -const cacheCmdLong = `Contains sub-commands for managing project cache. +const CacheCmdLong = `Contains sub-commands for managing project cache. The project cache keeps this speedy, without smashing against the Git API every time a new project is added / searched for` -const rootCmdLong = `Finds Git projects using fuzzy-find, remembering +const RootCmdLong = `Finds Git projects using fuzzy-find, remembering your chosen term for the project as an alias, and offers helpful shortcuts for moving around in projects and opening your code` -const projCmdLong = `Switches to a Git project by name or alias +const ProjCmdLong = `Switches to a Git project by name or alias If not found, will enter fzf mode. If not cloned, will clone the project locally.` -const projGoCmdLong = `Go to a project, searching by alias +const ProjGoCmdLong = `Go to a project, searching by alias If project is not already cloned, its path will be built and it will be cloned from source control. If conf.projects.alwaysPull, a git pull will be ran automatically` -const projRunCmdLong = `Runs the current project. Tries to detect +const ProjRunCmdLong = `Runs the current project. Tries to detect the language and runs accordingly (e.g. go run .)` -const projListCmdLong = `List locally cloned projects. Optionally +const ProjListCmdLong = `List locally cloned projects. Optionally lists all projects in project cache` -const projAddCmdLong = `Adds a new project to the local project path +const ProjAddCmdLong = `Adds a new project to the local project path uses fuzzy find to locate the project` -const projShowCmdLong = `Shows detail for a particular project +const ProjShowCmdLong = `Shows detail for a particular project Will always fuzzy find` -const projOpenCmdLong = `Opens the given project directory in the editor +const ProjOpenCmdLong = `Opens the given project directory in the editor of your choice. Will find certain well-known entrypoints (e.g. main.go). If your editor is set in your config file, it will be used, otherwise one will be found in your path from a list of known defaults.` -const configCmdLong = `Commands for managing configuration, particulary +const ConfigCmdLong = `Commands for managing configuration, particulary useful for seeding a new config file` -const configGenCmdLong = `Produces yaml to stdout that can be used +const ConfigGenCmdLong = `Produces yaml to stdout that can be used to seed the configuration file` diff --git a/cmd/util_fzf.go b/cmd/util/util_fzf.go similarity index 60% rename from cmd/util_fzf.go rename to cmd/util/util_fzf.go index 902b1f9..dc8a861 100644 --- a/cmd/util_fzf.go +++ b/cmd/util/util_fzf.go @@ -1,4 +1,4 @@ -package cmd +package util import ( "context" @@ -10,7 +10,7 @@ import ( "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" ) -type fzfProjectOpts struct { +type FzfProjectOpts struct { Ctx context.Context Search string MustHaveAlias bool @@ -19,14 +19,14 @@ type fzfProjectOpts struct { // This will try to find a project by alias if a search term // is given, otherwise will fuzzy find by project -func fzfFindProject(opts *fzfProjectOpts) *projects.Project { +func (u *Utils) FzfFindProject(opts *FzfProjectOpts) *projects.Project { var project *projects.Project if opts.Search != "" { - project = fzfSearchProjectAliases(opts) + project = u.FzfSearchProjectAliases(opts) } else { var err error - project, err = fzfProject(opts) + project, err = u.FzfProject(opts) if project == nil || err != nil { return nil } @@ -38,35 +38,35 @@ func fzfFindProject(opts *fzfProjectOpts) *projects.Project { // If . is given as a project, will open project from the // current working directory. Otherwise, will attempt to fuzzy-find // a project given a search term if provided -func fzfCwdOrSearchProjectAliases(opts *fzfProjectOpts) *projects.Project { +func (u *Utils) FzfCwdOrSearchProjectAliases(opts *FzfProjectOpts) *projects.Project { var project *projects.Project if opts.Search == "." { - project, _ = projectCache.GetProjectFromCwd() + project, _ = u.Cache().GetProjectFromCwd() } else { - project = fzfSearchProjectAliases(opts) + project = u.FzfSearchProjectAliases(opts) } return project } // This will fuzzy search only aliases, preferring an exact // match if one is given -func fzfSearchProjectAliases(opts *fzfProjectOpts) *projects.Project { +func (u *Utils) FzfSearchProjectAliases(opts *FzfProjectOpts) *projects.Project { var project *projects.Project var alias *cache.ProjectAlias - if alias = projectCache.GetAliasByName(opts.Search, opts.Remotes...); alias != nil { - project = projectCache.GetProjectByAlias(alias) - plog.Info("Perfect alias match... flawless") + if alias = u.Cache().GetAliasByName(opts.Search, opts.Remotes...); alias != nil { + project = u.Cache().GetProjectByAlias(alias) + u.Logger().Info("Perfect alias match... flawless") } else { // Get fuzzy if we don't have an exact match - aliases := projectCache.FuzzyFindAlias(opts.Search) + aliases := u.Cache().FuzzyFindAlias(opts.Search) if len(aliases) > 1 { // If multiple aliases were found, switch over to project // by alias mode with merging // alias = fzfAliasFromAliases(rootCmd.Context(), aliases) - project, _ = fzfProjectFromAliases(opts, aliases) + project, _ = u.FzfProjectFromAliases(opts, aliases) } else if len(aliases) == 1 { alias = aliases[0] - project = projectCache.GetProjectByAlias(alias) + project = u.Cache().GetProjectByAlias(alias) } } return project @@ -76,18 +76,18 @@ func fzfSearchProjectAliases(opts *fzfProjectOpts) *projects.Project { // a single one. Replaced by fzfProjectFromAliases in fzfSearchProjectAliases // as merging is preferred, but can be used if it's ever desirable to // return a single alias from all aliases -func fzfAliasFromAliases(opts *fzfProjectOpts, aliases []*cache.ProjectAlias) *cache.ProjectAlias { +func (u *Utils) FzfAliasFromAliases(opts *FzfProjectOpts, aliases []*cache.ProjectAlias) *cache.ProjectAlias { var alias *cache.ProjectAlias i, err := fzf.Find( aliases, func(i int) string { - return aliases[i].Alias + " -> " + projectCache.GetProjectByAlias(aliases[i]).PathWithNamespace + return aliases[i].Alias + " -> " + u.Cache().GetProjectByAlias(aliases[i]).PathWithNamespace }, fzf.WithContext(opts.Ctx), fzf.WithHeader("Choose an Alias"), ) if err != nil { - plog.Error("Failed to fzf alias slice", plog.Args("error", err)) + u.Logger().Error("Failed to fzf alias slice", u.Logger().Args("error", err)) } else { alias = aliases[i] } @@ -96,21 +96,21 @@ func fzfAliasFromAliases(opts *fzfProjectOpts, aliases []*cache.ProjectAlias) *c // Given a list of aliases, merge them together and use the resulting // list of projects to return a project -func fzfProjectFromAliases(opts *fzfProjectOpts, aliases []*cache.ProjectAlias) ( +func (u *Utils) FzfProjectFromAliases(opts *FzfProjectOpts, aliases []*cache.ProjectAlias) ( *projects.Project, error, ) { - mergedProjects := projectsFromAliases(aliases) + mergedProjects := u.projectsFromAliases(aliases) if len(mergedProjects) == 1 { return mergedProjects[0], nil } - return fzfProjectFromProjects(opts, mergedProjects) + return u.FzfProjectFromProjects(opts, mergedProjects) } -func projectsFromAliases(aliases []*cache.ProjectAlias) []*projects.Project { +func (u *Utils) projectsFromAliases(aliases []*cache.ProjectAlias) []*projects.Project { projects := make([]*projects.Project, 0, len(aliases)) for _, a := range aliases { - project := projectCache.GetProjectByAlias(a) + project := u.Cache().GetProjectByAlias(a) if project != nil && !slices.Contains(projects, project) { projects = append(projects, project) } @@ -121,30 +121,30 @@ func projectsFromAliases(aliases []*cache.ProjectAlias) []*projects.Project { // If opts.MustHaveAlias, will only allow selection of projects // that have at least one alias defined -func fzfProject(opts *fzfProjectOpts) (*projects.Project, error) { +func (u *Utils) FzfProject(opts *FzfProjectOpts) (*projects.Project, error) { var searchableProjects []*projects.Project if opts.MustHaveAlias { - searchableProjects = projectCache.GetProjectsWithAliases() + searchableProjects = u.Cache().GetProjectsWithAliases() } else { - searchableProjects = projectCache.Projects + searchableProjects = u.Cache().Projects } // Filter out unwanted remotes if provided - searchableProjects = filterProjectsWithRemotes(searchableProjects, opts.Remotes...) - return fzfProjectFromProjects(opts, searchableProjects) + searchableProjects = u.FilterProjectsWithRemotes(searchableProjects, opts.Remotes...) + return u.FzfProjectFromProjects(opts, searchableProjects) } // Takes a list of projects and performs a fuzzyfind -func fzfProjectFromProjects(opts *fzfProjectOpts, projects []*projects.Project) ( +func (u *Utils) FzfProjectFromProjects(opts *FzfProjectOpts, projects []*projects.Project) ( *projects.Project, error, ) { i, err := fzf.Find(projects, func(i int) string { // Display the project along with its aliases - return projectCache.GetProjectStringWithAliases(projects[i]) + return u.Cache().GetProjectStringWithAliases(projects[i]) }, fzf.WithPreviewWindow( func(i, width, height int) string { - return projectCache.ProjectString(projects[i]) + return u.Cache().ProjectString(projects[i]) }, ), fzf.WithContext(opts.Ctx), @@ -156,12 +156,12 @@ func fzfProjectFromProjects(opts *fzfProjectOpts, projects []*projects.Project) return projects[i], nil } -func fzfPreviewWindow(i, _, _ int) string { - p := projectCache.Projects[i] - return projectCache.ProjectString(p) +func (u *Utils) FzfPreviewWindow(i, _, _ int) string { + p := u.Cache().Projects[i] + return u.Cache().ProjectString(p) } -func filterProjectsWithRemotes(gitProjects []*projects.Project, remotes ...string) []*projects.Project { +func (u *Utils) FilterProjectsWithRemotes(gitProjects []*projects.Project, remotes ...string) []*projects.Project { filteredProjects := make([]*projects.Project, 0, len(gitProjects)) if len(remotes) > 0 { for _, p := range gitProjects { @@ -177,7 +177,7 @@ func filterProjectsWithRemotes(gitProjects []*projects.Project, remotes ...strin // Nearly useless function that simply returns either an // empty string, or a string from the first arg if one is provided -func searchStringFromArgs(args []string) string { +func (u *Utils) SearchStringFromArgs(args []string) string { var term string if len(args) > 0 { term = args[0] diff --git a/cmd/util/util_init.go b/cmd/util/util_init.go new file mode 100644 index 0000000..f01a6a3 --- /dev/null +++ b/cmd/util/util_init.go @@ -0,0 +1,163 @@ +// This file contains init methods that may be used by +// multiple sub-commands. For instance, the cach and projects +// sub-commands both depend on a cache and may both call the initProjectCache +// func (u *Util) from their PersistentPreRun commands +package util + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + "golang.org/x/sys/unix" + + "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/config" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes" + gitearemote "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/gitea" + githubremote "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/github" + gitlabremote "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/gitlab" + "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/remote" +) + +func InitProjects(cmd *cobra.Command, args []string) { + utils, _ := FromCtx(cmd.Context()) + utils.InitProjectCache(cmd, args) + utils.mustHaveProjects(cmd, args) +} + +func (u *Utils) mustHaveProjects(cmd *cobra.Command, _ []string) { + if len(u.Cache().Projects) == 0 { + u.Logger().Fatal("No projects to " + cmd.Name() + ", try running cache load") + } +} + +func (u *Utils) InitProjectCache(cmd *cobra.Command, _ []string) { + var err error + u.Logger().Debug("Running pre-run for cacheCmd") + u.Config().Cache.File = u.Config().ProjectPath + "/.cache.yaml" + + gitRemotes := remotes.NewRemotes() + gitRemotes.AddRemotes(*u.GetRemotes(cmd)...) + + cacheOpts := &cache.CacheOpts{ + ProjectsPath: u.Config().ProjectPath, + Path: u.Config().Cache.File, + TTL: u.Config().Cache.Ttl, + Logger: u.Logger(), + Remotes: gitRemotes, + Config: u.Config(), + } + + projectCache, err := cache.NewProjectCache(cacheOpts) + if err != nil { + u.Logger().Error("Failed to prepare project cache", u.Logger().Args("error", err)) + os.Exit(1) + } + u.SetCache(projectCache) + + if err := u.Cache().Read(); err != nil { + u.Logger().Error("Cache load failed", u.Logger().Args("error", err)) + os.Exit(1) + } + + u.Logger().Debug("Remotes Loaded", u.Logger().Args("remotes", cacheOpts.Remotes)) +} + +// Generically loads remotes from info.RemoteInfo in config.Remotes +func (u *Utils) GetRemotes(cmd *cobra.Command) *remotes.Remotes { + gitRemotes := new(remotes.Remotes) + *gitRemotes = make([]remote.Remote, 0) + for _, r := range u.Config().Remotes { + // Create a copy, set context + gitRemoteInfo := r + gitRemoteInfo.SetContext(cmd.Context()) + var gitRemote remote.Remote + var err error + switch r.Type { + case "gitlab": + gitRemote, err = gitlabremote.NewGitlabRemote(&gitRemoteInfo) + case "gitea": + gitRemote, err = gitearemote.NewGiteaRemote(&gitRemoteInfo) + case "github": + gitRemote, err = githubremote.NewGithubRemote(&gitRemoteInfo) + } + if err != nil { + u.Logger().Error("Failed to prepare remote", u.Logger().Args( + "error", err, + "type", r.Type)) + } else { + *gitRemotes = append(*gitRemotes, gitRemote) + } + } + return gitRemotes +} + +func PostProjectCmd(cmd *cobra.Command, args []string) { + utils, _ := FromCtx(cmd.Context()) + utils.PostProjectCache(cmd, args) +} + +func (u *Utils) PostProjectCache(_ *cobra.Command, _ []string) { + u.Cache().Write() +} + +func (u *Utils) InitProjectPath(_ *cobra.Command, _ []string) { + u.Logger().Debug("Running persistent pre-run for rootCmd") + var err error + if u.Config().ProjectPath == "" { + u.Config().ProjectPath = config.DefaultConfig.ProjectPath + return + } + if u.Config().ProjectPath, err = ResolvePath(u.Config().ProjectPath); err != nil { + u.Logger().Error("Failed to determine project path", u.Logger().Args("error", err)) + os.Exit(1) + } + _, err = os.Stat(u.Config().ProjectPath) + if err != nil { + u.Logger().Error("Failed to stat project path, trying to create", u.Logger().Args("error", err)) + if err = os.MkdirAll(u.Config().ProjectPath, 0o750); err != nil { + u.Logger().Error("Failed to create project path", u.Logger().Args("error", err)) + os.Exit(1) + } + u.Logger().Info("Project path created", u.Logger().Args("path", u.Config().ProjectPath)) + } else { + if err = unix.Access(u.Config().ProjectPath, unix.W_OK); err != nil { + u.Logger().Error("Unable to write to project path", u.Logger().Args( + "path", u.Config().ProjectPath, + "error", err)) + os.Exit(1) + } + } +} + +func ResolvePath(path string) (string, error) { + if strings.HasPrefix(path, "~/") { + usr, _ := user.Current() + path = filepath.Join(usr.HomeDir, path[2:]) + } + return filepath.Abs(path) +} + +func GetConfigName(configPath string) string { + // Check existing config + for _, ext := range []string{"yml", "yaml"} { + configFile := fmt.Sprintf("%s/%s.%s", configPath, ConfigName, ext) + legacyConfigFile := fmt.Sprintf("%s/%s.%s", configPath, LegacyConfigName, ext) + + if _, err := os.Stat(configFile); err == nil { + return ConfigName + } else if _, err := os.Stat(legacyConfigFile); err == nil { + pterm.DefaultLogger.WithWriter(os.Stderr). + Warn(fmt.Sprintf("using legacy config path, suggest using %s/%s.yaml", configPath, ConfigName)) + return LegacyConfigName + } + } + + // Nothing found, do what we want + return ConfigName +} diff --git a/cmd/util_completion.go b/cmd/util_completion.go deleted file mode 100644 index e07b2b5..0000000 --- a/cmd/util_completion.go +++ /dev/null @@ -1,64 +0,0 @@ -package cmd - -import ( - "strconv" - "strings" - - "github.com/spf13/cobra" - "golang.org/x/exp/slices" -) - -func validProjectsFunc(cmd *cobra.Command, args []string, toComplete string) ( - []string, cobra.ShellCompDirective) { - initProjectCache(cmd, args) - return projectCache.ProjectStrings(toComplete), cobra.ShellCompDirectiveNoFileComp -} - -func validAliasesFunc(cmd *cobra.Command, args []string, toComplete string) ( - []string, cobra.ShellCompDirective) { - initProjectCache(cmd, args) - return projectCache.AliasStrings(toComplete), cobra.ShellCompDirectiveNoFileComp -} - -func validProjectsOrAliasesFunc(cmd *cobra.Command, args []string, toComplete string) ( - []string, cobra.ShellCompDirective) { - projectStrings, _ := validAliasesFunc(cmd, args, toComplete) - aliasStrings, _ := validProjectsFunc(cmd, args, toComplete) - return append(projectStrings, aliasStrings...), cobra.ShellCompDirectiveDefault -} - -func validRemotesFunc(cmd *cobra.Command, args []string, toComplete string) ( - []string, cobra.ShellCompDirective) { - remotes := make([]string, 0, len(conf.Remotes)) - for _, remote := range conf.Remotes { - if strings.HasPrefix(remote.Host, toComplete) { - remotes = append(remotes, remote.Host) - } - } - return remotes, cobra.ShellCompDirectiveNoFileComp -} - -func validLogLevelsFunc(cmd *cobra.Command, args []string, toComplete string) ( - []string, cobra.ShellCompDirective) { - levels := []string{"info", "warn", "error", "debug"} - matchingLevels := make([]string, 0, len(levels)) - for _, level := range levels { - if strings.HasPrefix(level, toComplete) { - matchingLevels = append(matchingLevels, level) - } - } - return matchingLevels, cobra.ShellCompDirectiveNoFileComp -} - -func validProjectIdFunc(cmd *cobra.Command, args []string, toComplete string) ( - []string, cobra.ShellCompDirective) { - initProjectCache(cmd, args) - matchingIds := make([]string, 0, len(projectCache.Projects)) - for _, p := range projectCache.Projects { - idString := strconv.FormatInt(int64(p.ID), 10) - if strings.HasPrefix(idString, toComplete) { - matchingIds = append(matchingIds, idString) - } - } - return slices.Clip(matchingIds), cobra.ShellCompDirectiveNoFileComp -} diff --git a/cmd/util_init.go b/cmd/util_init.go deleted file mode 100644 index 18aaebb..0000000 --- a/cmd/util_init.go +++ /dev/null @@ -1,143 +0,0 @@ -// This file contains init methods that may be used by -// multiple sub-commands. For instance, the cach and projects -// sub-commands both depend on a cache and may both call the initProjectCache -// func from their PersistentPreRun commands -package cmd - -import ( - "fmt" - "os" - "os/user" - "path/filepath" - "strings" - - "github.com/pterm/pterm" - "github.com/spf13/cobra" - "golang.org/x/sys/unix" - - "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache" - "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/config" - "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes" - gitearemote "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/gitea" - githubremote "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/github" - gitlabremote "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/gitlab" - "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/remote" -) - -func initProjectCache(cmd *cobra.Command, _ []string) { - var err error - plog.Debug("Running pre-run for cacheCmd") - conf.Cache.File = conf.ProjectPath + "/.cache.yaml" - - gitRemotes := remotes.NewRemotes() - gitRemotes.AddRemotes(*getRemotes(cmd)...) - - cacheOpts := &cache.CacheOpts{ - ProjectsPath: conf.ProjectPath, - Path: conf.Cache.File, - TTL: conf.Cache.Ttl, - Logger: plog, - Remotes: gitRemotes, - Config: &conf, - } - if projectCache, err = cache.NewProjectCache(cacheOpts); err != nil { - plog.Error("Failed to prepare project cache", plog.Args("error", err)) - os.Exit(1) - } - - if err := projectCache.Read(); err != nil { - plog.Error("Cache load failed", plog.Args("error", err)) - os.Exit(1) - } - - plog.Debug("Remotes Loaded", plog.Args("remotes", cacheOpts.Remotes)) -} - -// Generically loads remotes from info.RemoteInfo in config.Remotes -func getRemotes(cmd *cobra.Command) *remotes.Remotes { - gitRemotes := new(remotes.Remotes) - *gitRemotes = make([]remote.Remote, 0) - for _, r := range conf.Remotes { - // Create a copy, set context - gitRemoteInfo := r - gitRemoteInfo.SetContext(cmd.Context()) - var gitRemote remote.Remote - var err error - switch r.Type { - case "gitlab": - gitRemote, err = gitlabremote.NewGitlabRemote(&gitRemoteInfo) - case "gitea": - gitRemote, err = gitearemote.NewGiteaRemote(&gitRemoteInfo) - case "github": - gitRemote, err = githubremote.NewGithubRemote(&gitRemoteInfo) - } - if err != nil { - plog.Error("Failed to prepare remote", plog.Args( - "error", err, - "type", r.Type)) - } else { - *gitRemotes = append(*gitRemotes, gitRemote) - } - } - return gitRemotes -} - -func postProjectCache(_ *cobra.Command, _ []string) { - projectCache.Write() -} - -func initProjectPath(_ *cobra.Command, _ []string) { - plog.Debug("Running persistent pre-run for rootCmd") - var err error - if conf.ProjectPath == "" { - conf.ProjectPath = config.DefaultConfig.ProjectPath - return - } - if conf.ProjectPath, err = resolvePath(conf.ProjectPath); err != nil { - plog.Error("Failed to determine project path", plog.Args("error", err)) - os.Exit(1) - } - _, err = os.Stat(conf.ProjectPath) - if err != nil { - plog.Error("Failed to stat project path, trying to create", plog.Args("error", err)) - if err = os.MkdirAll(conf.ProjectPath, 0o750); err != nil { - plog.Error("Failed to create project path", plog.Args("error", err)) - os.Exit(1) - } - plog.Info("Project path created", plog.Args("path", conf.ProjectPath)) - } else { - if err = unix.Access(conf.ProjectPath, unix.W_OK); err != nil { - plog.Error("Unable to write to project path", plog.Args( - "path", conf.ProjectPath, - "error", err)) - os.Exit(1) - } - } -} - -func resolvePath(path string) (string, error) { - if strings.HasPrefix(path, "~/") { - usr, _ := user.Current() - path = filepath.Join(usr.HomeDir, path[2:]) - } - return filepath.Abs(path) -} - -func getConfigName(configPath string) string { - // Check existing config - for _, ext := range []string{"yml", "yaml"} { - configFile := fmt.Sprintf("%s/%s.%s", configPath, configName, ext) - legacyConfigFile := fmt.Sprintf("%s/%s.%s", configPath, legacyConfigName, ext) - - if _, err := os.Stat(configFile); err == nil { - return configName - } else if _, err := os.Stat(legacyConfigFile); err == nil { - pterm.DefaultLogger.WithWriter(os.Stderr). - Warn(fmt.Sprintf("using legacy config path, suggest using %s/%s.yaml", configPath, configName)) - return legacyConfigName - } - } - - // Nothing found, do what we want - return configName -}