refactor-cmd-pkgs #1

Merged
rmcguire merged 2 commits from refactor-cmd-pkgs into main 2024-12-30 20:56:39 +00:00
19 changed files with 564 additions and 412 deletions
Showing only changes of commit 96378d047e - Show all commits

View File

@ -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())
}
}

37
cmd/alias/alias.go Normal file
View File

@ -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)
}

View File

@ -1,4 +1,4 @@
package cmd package alias
import ( import (
"fmt" "fmt"
@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "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/cache"
"gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects"
) )
@ -17,7 +18,7 @@ var aliasAddCmd = &cobra.Command{
Aliases: []string{"set", "a", "s"}, Aliases: []string{"set", "a", "s"},
Short: "Add a project alias", Short: "Add a project alias",
Args: cobra.ArbitraryArgs, Args: cobra.ArbitraryArgs,
Long: aliasAddCmdLong, Long: util.AliasAddCmdLong,
Run: runAddAliasCmd, Run: runAddAliasCmd,
} }
@ -25,22 +26,22 @@ func runAddAliasCmd(cmd *cobra.Command, args []string) {
var project *projects.Project var project *projects.Project
// Check by flag // Check by flag
if projectID := viper.GetInt(ViperAliasAddPID); projectID > 0 { if projectID := viper.GetInt(util.ViperAliasAddPID); projectID > 0 {
plog.Debug(fmt.Sprintf("Adding for inbound project ID %d", projectID)) utils.Logger().Debug(fmt.Sprintf("Adding for inbound project ID %d", projectID))
project = projectCache.GetProjectByID(projectID) project = utils.Cache().GetProjectByID(projectID)
} }
// Check by arg // Check by arg
if len(args) > 0 { 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 // Collect by fzf
if project == nil { if project == nil {
var err error var err error
project, err = fzfProject(&fzfProjectOpts{Ctx: cmd.Context()}) project, err = utils.FzfProject(&util.FzfProjectOpts{Ctx: cmd.Context()})
if err != nil || project == nil { 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) { func addNewAliases(projectID int) {
project := projectCache.GetProjectByID(projectID) project := utils.Cache().GetProjectByID(projectID)
if project == nil { 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 return
} }
@ -63,13 +64,13 @@ func addNewAliases(projectID int) {
if a == "" { if a == "" {
continue continue
} }
if err := projectCache.AddAlias(a, project.ID, project.Remote); err != nil { if err := utils.Cache().AddAlias(a, project.ID, project.Remote); err != nil {
plog.Debug("Skipping alias add", plog.Args( utils.Logger().Debug("Skipping alias add", utils.Logger().Args(
"error", err, "error", err,
"alias", a, "alias", a,
)) ))
} else { } else {
plog.Info("Successfully added alias to project", plog.Args( utils.Logger().Info("Successfully added alias to project", utils.Logger().Args(
"project", project.String(), "project", project.String(),
"alias", a, "alias", a,
)) ))
@ -78,9 +79,9 @@ func addNewAliases(projectID int) {
} }
func promptAliasesForProject(p *projects.Project) []string { func promptAliasesForProject(p *projects.Project) []string {
aliases := projectCache.GetProjectAliases(p) aliases := utils.Cache().GetProjectAliases(p)
if len(aliases) > 0 { 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(), "project", p.String(),
"existingAliases", cache.ProjectAliasesString(aliases), "existingAliases", cache.ProjectAliasesString(aliases),
)) ))
@ -97,10 +98,9 @@ func promptAliasesForProject(p *projects.Project) []string {
} }
func init() { func init() {
aliasCmd.AddCommand(aliasAddCmd) aliasAddCmd.PersistentFlags().Int(util.FlagProjectID, 0, "Specify a project by ID")
aliasAddCmd.PersistentFlags().Int(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))
} }

View File

@ -1,19 +1,20 @@
package cmd package alias
import ( import (
"fmt" "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/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects"
) )
var aliasDeleteCmd = &cobra.Command{ var aliasDeleteCmd = &cobra.Command{
Use: "delete [fuzzy project or alias]", Use: "delete [fuzzy project or alias]",
Aliases: []string{"rm", "del", "d"}, Aliases: []string{"rm", "del", "d"},
Short: "Delete a project alias", Short: "Delete a project alias",
Long: aliasDeleteCmdLong, Long: util.AliasDeleteCmdLong,
PreRun: mustHaveAliases, PreRun: mustHaveAliases,
Run: runDeleteAliasCmd, Run: runDeleteAliasCmd,
} }
@ -22,32 +23,32 @@ func runDeleteAliasCmd(cmd *cobra.Command, args []string) {
var project *projects.Project var project *projects.Project
var err error var err error
fzfOpts := &fzfProjectOpts{ fzfOpts := &util.FzfProjectOpts{
Ctx: cmd.Context(), Ctx: cmd.Context(),
MustHaveAlias: true, MustHaveAlias: true,
} }
if len(args) > 0 { if len(args) > 0 {
fzfOpts.Search = searchStringFromArgs(args) fzfOpts.Search = utils.SearchStringFromArgs(args)
project = fzfFindProject(fzfOpts) project = utils.FzfFindProject(fzfOpts)
} else { } else {
project, err = fzfProject(fzfOpts) project, err = utils.FzfProject(fzfOpts)
} }
if project == nil || err != nil { 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, "error", err,
)) ))
} }
aliasStrings := projectCache.GetProjectAliasStrings(project) aliasStrings := utils.Cache().GetProjectAliasStrings(project)
deletionCandidates, err := pterm.DefaultInteractiveMultiselect. deletionCandidates, err := pterm.DefaultInteractiveMultiselect.
WithOptions(aliasStrings). WithOptions(aliasStrings).
Show() Show()
if err != nil || len(deletionCandidates) < 1 { 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, "error", err,
)) ))
} }
@ -61,26 +62,25 @@ func runDeleteAliasCmd(cmd *cobra.Command, args []string) {
Show() Show()
if !confirm { if !confirm {
plog.Warn("Alias deletion cancelled") utils.Logger().Warn("Alias deletion cancelled")
continue continue
} }
plog.Info("Deleting alias", plog.Args( utils.Logger().Info("Deleting alias", utils.Logger().Args(
"project", project.String(), "project", project.String(),
"alias", a, "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() { func init() {
aliasCmd.AddCommand(aliasDeleteCmd)
aliasDeleteCmd.PersistentFlags().Int("projectID", 0, "Specify a project by ID") 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")) viper.BindPFlag("alias.delete.projectID", aliasDeleteCmd.Flag("projectID"))
} }

View File

@ -1,8 +1,9 @@
package cmd package alias
import ( import (
"fmt" "fmt"
"gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -14,7 +15,7 @@ var aliasListCmd = &cobra.Command{
Aliases: []string{"dump", "show", "ls", "ll", "l"}, Aliases: []string{"dump", "show", "ls", "ll", "l"},
Short: "List Aliases", Short: "List Aliases",
PreRun: mustHaveAliases, PreRun: mustHaveAliases,
Long: aliasListCmdLong, Long: util.AliasListCmdLong,
Run: runListAliasCmd, Run: runListAliasCmd,
} }

View File

@ -5,12 +5,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache"
) )
var projectCache *cache.Cache
var cacheCmd = &cobra.Command{ var cacheCmd = &cobra.Command{
Use: "cache", Use: "cache",
Aliases: []string{"a", "ln"}, Aliases: []string{"a", "ln"},

View File

@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "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/cache"
"gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects"
) )
@ -15,16 +16,16 @@ var projectCmd = &cobra.Command{
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
ArgAliases: []string{"alias"}, ArgAliases: []string{"alias"},
Long: projCmdLong, Long: projCmdLong,
PersistentPreRun: initProjectCmd, PersistentPreRun: initProjects,
PersistentPostRun: postProjectCmd, PersistentPostRun: postProjectCmd,
// Run: projectGoCmdRun, // Run: projectGoCmdRun,
} }
func getProject(args []string) *projects.Project { func getProject(args []string) *projects.Project {
remotes := viper.GetStringSlice(FlagRemote) remotes := viper.GetStringSlice(util.FlagRemote)
fzfOpts := &fzfProjectOpts{ fzfOpts := &util.FzfProjectOpts{
Ctx: rootCmd.Context(), Ctx: rootCmd.Context(),
Search: searchStringFromArgs(args), Search: utils.SearchStringFromArgs(args),
Remotes: remotes, Remotes: remotes,
} }
project := fzfFindProject(fzfOpts) project := fzfFindProject(fzfOpts)
@ -47,21 +48,7 @@ func getProject(args []string) *projects.Project {
return 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() { func init() {
rootCmd.AddCommand(projectCmd) 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")
}
}

View File

@ -12,28 +12,27 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util"
"gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/config" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/config"
) )
var (
conf config.Config
plog *pterm.Logger
)
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "git-project-manager", Use: "git-project-manager",
Aliases: []string{"gpm"}, Aliases: []string{"gpm"},
Short: "Find and use Git projects locally", Short: "Find and use Git projects locally",
Long: rootCmdLong, Long: util.RootCmdLong,
PersistentPreRun: initRootCmd, 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 // Hook traversal is enabled, so this will be run for all
// sub-commands regardless of their registered pre-hooks // sub-commands regardless of their registered pre-hooks
func initRootCmd(cmd *cobra.Command, args []string) { 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. // 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) ctx, cncl := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt)
defer cncl() defer cncl()
// Store provided util.Utils in context
ctx = util.AddToCtx(ctx, utils)
err := rootCmd.ExecuteContext(ctx) err := rootCmd.ExecuteContext(ctx)
if err != nil { if err != nil {
pterm.Error.Printfln("%s", pterm.LightYellow("Command failed, "+err.Error())) pterm.Error.Printfln("%s", pterm.LightYellow("Command failed, "+err.Error()))
@ -51,73 +53,79 @@ func Execute() {
func init() { func init() {
cobra.EnableTraverseRunHooks = true cobra.EnableTraverseRunHooks = true
cobra.OnInitialize(initConfig)
utils = &util.Utils{}
cobra.OnInitialize(getInitConfigFunc(utils))
// Global flags // Global flags
rootCmd.PersistentFlags().String(FlagConfig, "", rootCmd.PersistentFlags().String(util.FlagConfig, "",
"config file (default is "+defConfigPath+")") "config file (default is "+util.DefConfigPath+")")
rootCmd.PersistentFlags().String(FlagPath, "", rootCmd.PersistentFlags().String(util.FlagPath, "",
"Sets a path for local clones of projects") "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") "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.") "Specify remotes by host for any sub-command. Provide multiple times or comma delimited.")
// Flag autocompletion // Flag autocompletion
rootCmd.RegisterFlagCompletionFunc(FlagLogLevel, validLogLevelsFunc) rootCmd.RegisterFlagCompletionFunc(util.FlagLogLevel, util.ValidLogLevelsFunc)
rootCmd.RegisterFlagCompletionFunc(FlagRemote, validRemotesFunc) rootCmd.RegisterFlagCompletionFunc(util.FlagRemote, utils.ValidRemotesFunc)
viper.BindPFlags(rootCmd.PersistentFlags()) viper.BindPFlags(rootCmd.PersistentFlags())
} }
// initConfig reads in config file and ENV variables if set. // initConfig reads in config file and ENV variables if set.
func initConfig() { func getInitConfigFunc(utils *util.Utils) func() {
cfgFile := viper.GetString(FlagConfig) return func() {
if cfgFile != "" { cfgFile := viper.GetString(util.FlagConfig)
// Use config file from the flag. if cfgFile != "" {
viper.SetConfigFile(cfgFile) // Use config file from the flag.
} else { viper.SetConfigFile(cfgFile)
// Find home directory. } else {
home, err := os.UserHomeDir() // Find home directory.
cobra.CheckErr(err) home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".git-project-manager" (without extension). // Search config in home directory with name ".git-project-manager" (without extension).
configPath := filepath.Join(home, ".config") configPath := filepath.Join(home, ".config")
viper.AddConfigPath(configPath) viper.AddConfigPath(configPath)
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
viper.SetConfigName(getConfigName(configPath)) 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 { func getPtermLogLevel(level string) pterm.LogLevel {
@ -141,12 +149,12 @@ func getPtermLogLevel(level string) pterm.LogLevel {
func checkConfigPerms(file string) { func checkConfigPerms(file string) {
stat, err := os.Stat(file) stat, err := os.Stat(file)
if err != nil { if err != nil {
plog.Error("Failure reading configuration", plog.Args("err", err)) utils.Logger().Error("Failure reading configuration", utils.Logger().Args("err", err))
return return
} }
if stat.Mode().Perm()&0o004 == 0o004 { if stat.Mode().Perm()&0o004 == 0o004 {
plog.Error("Configuration is world-readable. Recomment 0400.", utils.Logger().Error("Configuration is world-readable. Recomment 0400.",
plog.Args("mode", stat.Mode().String())) utils.Logger().Args("mode", stat.Mode().String()))
os.Exit(1) os.Exit(1)
} }
} }

121
cmd/util/util.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -1,4 +1,4 @@
package cmd package util
const ( const (
// Cobra Flags // Cobra Flags
@ -25,62 +25,62 @@ const (
) )
const ( const (
defGitlabHost = "https://gitlab.com" DefGitlabHost = "https://gitlab.com"
defLogLevel = "info" DefLogLevel = "info"
defConfigPath = "~/.config/git-project-manager.yaml" DefConfigPath = "~/.config/git-project-manager.yaml"
configName = "git-project-manager" ConfigName = "git-project-manager"
legacyConfigName = "gitlab-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.` 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` 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` 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 The project cache keeps this speedy, without smashing against the Git
API every time a new project is added / searched for` 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 your chosen term for the project as an alias, and offers helpful
shortcuts for moving around in projects and opening your code` 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 If not found, will enter fzf mode. If not cloned, will clone
the project locally.` 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 If project is not already cloned, its path will be built and it
will be cloned from source control. will be cloned from source control.
If conf.projects.alwaysPull, a git pull will be ran automatically` 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 .)` 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` 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` 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` 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). 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 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.` 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` 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` to seed the configuration file`

View File

@ -1,4 +1,4 @@
package cmd package util
import ( import (
"context" "context"
@ -10,7 +10,7 @@ import (
"gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects"
) )
type fzfProjectOpts struct { type FzfProjectOpts struct {
Ctx context.Context Ctx context.Context
Search string Search string
MustHaveAlias bool MustHaveAlias bool
@ -19,14 +19,14 @@ type fzfProjectOpts struct {
// This will try to find a project by alias if a search term // This will try to find a project by alias if a search term
// is given, otherwise will fuzzy find by project // 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 var project *projects.Project
if opts.Search != "" { if opts.Search != "" {
project = fzfSearchProjectAliases(opts) project = u.FzfSearchProjectAliases(opts)
} else { } else {
var err error var err error
project, err = fzfProject(opts) project, err = u.FzfProject(opts)
if project == nil || err != nil { if project == nil || err != nil {
return nil return nil
} }
@ -38,35 +38,35 @@ func fzfFindProject(opts *fzfProjectOpts) *projects.Project {
// If . is given as a project, will open project from the // If . is given as a project, will open project from the
// current working directory. Otherwise, will attempt to fuzzy-find // current working directory. Otherwise, will attempt to fuzzy-find
// a project given a search term if provided // 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 var project *projects.Project
if opts.Search == "." { if opts.Search == "." {
project, _ = projectCache.GetProjectFromCwd() project, _ = u.Cache().GetProjectFromCwd()
} else { } else {
project = fzfSearchProjectAliases(opts) project = u.FzfSearchProjectAliases(opts)
} }
return project return project
} }
// This will fuzzy search only aliases, preferring an exact // This will fuzzy search only aliases, preferring an exact
// match if one is given // match if one is given
func fzfSearchProjectAliases(opts *fzfProjectOpts) *projects.Project { func (u *Utils) FzfSearchProjectAliases(opts *FzfProjectOpts) *projects.Project {
var project *projects.Project var project *projects.Project
var alias *cache.ProjectAlias var alias *cache.ProjectAlias
if alias = projectCache.GetAliasByName(opts.Search, opts.Remotes...); alias != nil { if alias = u.Cache().GetAliasByName(opts.Search, opts.Remotes...); alias != nil {
project = projectCache.GetProjectByAlias(alias) project = u.Cache().GetProjectByAlias(alias)
plog.Info("Perfect alias match... flawless") u.Logger().Info("Perfect alias match... flawless")
} else { } else {
// Get fuzzy if we don't have an exact match // 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 len(aliases) > 1 {
// If multiple aliases were found, switch over to project // If multiple aliases were found, switch over to project
// by alias mode with merging // by alias mode with merging
// alias = fzfAliasFromAliases(rootCmd.Context(), aliases) // alias = fzfAliasFromAliases(rootCmd.Context(), aliases)
project, _ = fzfProjectFromAliases(opts, aliases) project, _ = u.FzfProjectFromAliases(opts, aliases)
} else if len(aliases) == 1 { } else if len(aliases) == 1 {
alias = aliases[0] alias = aliases[0]
project = projectCache.GetProjectByAlias(alias) project = u.Cache().GetProjectByAlias(alias)
} }
} }
return project return project
@ -76,18 +76,18 @@ func fzfSearchProjectAliases(opts *fzfProjectOpts) *projects.Project {
// a single one. Replaced by fzfProjectFromAliases in fzfSearchProjectAliases // a single one. Replaced by fzfProjectFromAliases in fzfSearchProjectAliases
// as merging is preferred, but can be used if it's ever desirable to // as merging is preferred, but can be used if it's ever desirable to
// return a single alias from all aliases // 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 var alias *cache.ProjectAlias
i, err := fzf.Find( i, err := fzf.Find(
aliases, aliases,
func(i int) string { 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.WithContext(opts.Ctx),
fzf.WithHeader("Choose an Alias"), fzf.WithHeader("Choose an Alias"),
) )
if err != nil { 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 { } else {
alias = aliases[i] 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 // Given a list of aliases, merge them together and use the resulting
// list of projects to return a project // 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, *projects.Project, error,
) { ) {
mergedProjects := projectsFromAliases(aliases) mergedProjects := u.projectsFromAliases(aliases)
if len(mergedProjects) == 1 { if len(mergedProjects) == 1 {
return mergedProjects[0], nil 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)) projects := make([]*projects.Project, 0, len(aliases))
for _, a := range aliases { for _, a := range aliases {
project := projectCache.GetProjectByAlias(a) project := u.Cache().GetProjectByAlias(a)
if project != nil && !slices.Contains(projects, project) { if project != nil && !slices.Contains(projects, project) {
projects = append(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 // If opts.MustHaveAlias, will only allow selection of projects
// that have at least one alias defined // 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 var searchableProjects []*projects.Project
if opts.MustHaveAlias { if opts.MustHaveAlias {
searchableProjects = projectCache.GetProjectsWithAliases() searchableProjects = u.Cache().GetProjectsWithAliases()
} else { } else {
searchableProjects = projectCache.Projects searchableProjects = u.Cache().Projects
} }
// Filter out unwanted remotes if provided // Filter out unwanted remotes if provided
searchableProjects = filterProjectsWithRemotes(searchableProjects, opts.Remotes...) searchableProjects = u.FilterProjectsWithRemotes(searchableProjects, opts.Remotes...)
return fzfProjectFromProjects(opts, searchableProjects) return u.FzfProjectFromProjects(opts, searchableProjects)
} }
// Takes a list of projects and performs a fuzzyfind // 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, *projects.Project, error,
) { ) {
i, err := fzf.Find(projects, i, err := fzf.Find(projects,
func(i int) string { func(i int) string {
// Display the project along with its aliases // Display the project along with its aliases
return projectCache.GetProjectStringWithAliases(projects[i]) return u.Cache().GetProjectStringWithAliases(projects[i])
}, },
fzf.WithPreviewWindow( fzf.WithPreviewWindow(
func(i, width, height int) string { func(i, width, height int) string {
return projectCache.ProjectString(projects[i]) return u.Cache().ProjectString(projects[i])
}, },
), ),
fzf.WithContext(opts.Ctx), fzf.WithContext(opts.Ctx),
@ -156,12 +156,12 @@ func fzfProjectFromProjects(opts *fzfProjectOpts, projects []*projects.Project)
return projects[i], nil return projects[i], nil
} }
func fzfPreviewWindow(i, _, _ int) string { func (u *Utils) FzfPreviewWindow(i, _, _ int) string {
p := projectCache.Projects[i] p := u.Cache().Projects[i]
return projectCache.ProjectString(p) 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)) filteredProjects := make([]*projects.Project, 0, len(gitProjects))
if len(remotes) > 0 { if len(remotes) > 0 {
for _, p := range gitProjects { for _, p := range gitProjects {
@ -177,7 +177,7 @@ func filterProjectsWithRemotes(gitProjects []*projects.Project, remotes ...strin
// Nearly useless function that simply returns either an // Nearly useless function that simply returns either an
// empty string, or a string from the first arg if one is provided // 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 var term string
if len(args) > 0 { if len(args) > 0 {
term = args[0] term = args[0]

163
cmd/util/util_init.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}