From 4e76c9efe1b13aa072a214e3eeb0083f4925889b Mon Sep 17 00:00:00 2001 From: Ryan D McGuire Date: Fri, 8 Dec 2023 23:13:17 -0500 Subject: [PATCH] wip --- cmd/alias.go | 7 ++-- cmd/alias_add.go | 13 +++---- cmd/alias_delete.go | 54 ++++++++++++++++++++++++++---- cmd/alias_list.go | 24 +++++++++++++ cmd/config.go | 26 -------------- cmd/project.go | 14 ++++++++ cmd/project_go.go | 25 ++++++++++++++ cmd/project_show.go | 3 -- cmd/util_constants.go | 8 +++++ cmd/util_fzf.go | 28 +++++++++++----- cmd/util_init.go | 9 ++--- internal/gitlab/gitlab.go | 5 +++ internal/projects/cache.go | 13 ++++--- internal/projects/cache_aliases.go | 42 ++++++++++++++++++++++- internal/projects/fuzz.go | 2 +- internal/projects/projects.go | 27 +++++++++++++-- internal/projects/projects_fs.go | 30 +++++++++++++++++ 17 files changed, 265 insertions(+), 65 deletions(-) create mode 100644 cmd/alias_list.go delete mode 100644 cmd/config.go create mode 100644 cmd/project_go.go create mode 100644 internal/projects/projects_fs.go diff --git a/cmd/alias.go b/cmd/alias.go index 91e5a0c..fe1e028 100644 --- a/cmd/alias.go +++ b/cmd/alias.go @@ -5,9 +5,10 @@ import ( ) var aliasCmd = &cobra.Command{ - Use: "alias", - Short: "Manage project aliases", - Long: aliasCmdLong, + Use: "alias", + Aliases: []string{"aliases", "a"}, + Short: "Manage project aliases", + Long: aliasCmdLong, // Just re-use the hooks for project PersistentPreRun: initProjectCmd, PersistentPostRun: postProjectCmd, diff --git a/cmd/alias_add.go b/cmd/alias_add.go index 19341a0..2445cb7 100644 --- a/cmd/alias_add.go +++ b/cmd/alias_add.go @@ -12,11 +12,12 @@ import ( ) var aliasAddCmd = &cobra.Command{ - Use: "add", - Short: "Add a project alias", - Args: cobra.ArbitraryArgs, - Long: aliasAddCmdLong, - Run: runAddAliasCmd, + Use: "add", + Aliases: []string{"set", "a", "s"}, + Short: "Add a project alias", + Args: cobra.ArbitraryArgs, + Long: aliasAddCmdLong, + Run: runAddAliasCmd, } func runAddAliasCmd(cmd *cobra.Command, args []string) { @@ -57,7 +58,7 @@ func addNewAliases(projectID int) { // Add aliases for _, a := range aliases { - a = strings.Trim(a, " '\"%") + a = strings.Trim(a, " '\"%<>|`") if a == "" { continue } diff --git a/cmd/alias_delete.go b/cmd/alias_delete.go index 6f187ff..46b5e12 100644 --- a/cmd/alias_delete.go +++ b/cmd/alias_delete.go @@ -3,28 +3,70 @@ package cmd import ( "fmt" + "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" ) var aliasDeleteCmd = &cobra.Command{ - Use: "delete [fuzzy project or alias]", - Short: "Delete a project alias", - Long: aliasDeleteCmdLong, - Run: runDeleteAliasCmd, + Use: "delete [fuzzy project or alias]", + Aliases: []string{"rm", "del", "d"}, + Short: "Delete a project alias", + Long: aliasDeleteCmdLong, + Run: runDeleteAliasCmd, } func runDeleteAliasCmd(cmd *cobra.Command, args []string) { var project *gitlab.Project + var err error if len(args) > 0 { project = fzfFindProject(args[0]) } else { - project, _ = fzfProject(cmd.Context()) + project, err = fzfProject(cmd.Context(), true) } - fmt.Println(project.String()) + if project == nil || err != nil { + plog.Fatal("Failed to find project to delete aliases from", plog.Args( + "error", err, + )) + } + + aliasStrings := 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( + "error", err, + )) + } + + for _, a := range deletionCandidates { + + confirm, _ := pterm.DefaultInteractiveConfirm. + WithDefaultText(fmt.Sprintf("Really delete %s -> %s?", + a, project.String())). + WithConfirmText("y"). + Show() + + if !confirm { + plog.Warn("Alias deletion cancelled") + continue + } + + plog.Info("Deleting alias", plog.Args( + "project", project.String(), + "alias", a, + )) + + cache.DeleteAlias(cache.GetAliasByName(a)) + } + + fmt.Println(cache.ProjectString(project)) } func init() { diff --git a/cmd/alias_list.go b/cmd/alias_list.go new file mode 100644 index 0000000..a00d2f8 --- /dev/null +++ b/cmd/alias_list.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// listCmd represents the list command +var aliasListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"dump", "show", "ls", "ll", "l"}, + Short: "List Aliases", + Long: aliasListCmdLong, + Run: runListAliasCmd, +} + +func runListAliasCmd(cmd *cobra.Command, args []string) { + fmt.Print("\n" + cache.AliasesByProjectString() + "\n") +} + +func init() { + aliasCmd.AddCommand(aliasListCmd) +} diff --git a/cmd/config.go b/cmd/config.go deleted file mode 100644 index 503f194..0000000 --- a/cmd/config.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// configCmd represents the config command -var configCmd = &cobra.Command{ - Use: "config", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("config called") - }, -} - -func init() { - rootCmd.AddCommand(configCmd) -} diff --git a/cmd/project.go b/cmd/project.go index 0a0fb75..a0ee990 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -2,12 +2,14 @@ package cmd import ( "github.com/spf13/cobra" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/projects" ) var projectCmd = &cobra.Command{ Use: "project [fuzzy alias search]", Short: "Use a GitLab project", + Aliases: []string{"proj", "projects", "p"}, Args: cobra.MaximumNArgs(1), ArgAliases: []string{"alias"}, ValidArgsFunction: validAliasesFunc, @@ -18,10 +20,15 @@ var projectCmd = &cobra.Command{ } func projectCmdRun(cmd *cobra.Command, args []string) { + goToProject(getProject(args)) +} + +func getProject(args []string) *gitlab.Project { var searchString string if len(args) > 0 { searchString = args[0] } + project := fzfFindProject(searchString) if project == nil { @@ -33,6 +40,13 @@ func projectCmdRun(cmd *cobra.Command, args []string) { cache.GetProjectAliases(project)), )) } + + if len(cache.GetProjectAliases(project)) == 0 { + plog.Info("New project, set aliases or press enter for default") + addNewAliases(project.ID) + } + + return project } func initProjectCmd(cmd *cobra.Command, args []string) { diff --git a/cmd/project_go.go b/cmd/project_go.go new file mode 100644 index 0000000..f1d91e3 --- /dev/null +++ b/cmd/project_go.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" +) + +var projectGoCmd = &cobra.Command{ + Use: "go [fuzzy alias search]", + Short: "Go to a GitLab project", + Aliases: []string{"goto", "projects", "p"}, + Args: cobra.MaximumNArgs(1), + ArgAliases: []string{"project"}, + ValidArgsFunction: validAliasesFunc, + Long: projGoCmdLong, + Run: projectCmdRun, +} + +func projectGoCmdRun(cmd *cobra.Command, args []string) { + goToProject(getProject(args)) +} + +func goToProject(project *gitlab.Project) { + cache.GoTo(project) +} diff --git a/cmd/project_show.go b/cmd/project_show.go index 5bcd752..36edaa1 100644 --- a/cmd/project_show.go +++ b/cmd/project_show.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" - "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -34,9 +33,7 @@ func projectShowCmdRun(cmd *cobra.Command, args []string) { } } - fmt.Println(pterm.LightGreen("\n--------------")) fmt.Println(cache.ProjectString(project)) - fmt.Println(pterm.LightGreen("--------------\n")) } func init() { diff --git a/cmd/util_constants.go b/cmd/util_constants.go index 3798c69..db8eb43 100644 --- a/cmd/util_constants.go +++ b/cmd/util_constants.go @@ -9,6 +9,8 @@ const ( const aliasCmdLong = `Manages project aliases, with options for listing, adding, and deleting.` +const aliasListCmdLong = `Lists all aliases by project` + const aliasAddCmdLong = `Adds a project alias to a project project ID can be provided, or will otherwise use fuzzy find` @@ -27,5 +29,11 @@ const projCmdLong = `Switches to a GitLab 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 +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 projShowCmdLong = `Shows detail for a particular project Will always fuzzy find` diff --git a/cmd/util_fzf.go b/cmd/util_fzf.go index 64ecd99..8302a90 100644 --- a/cmd/util_fzf.go +++ b/cmd/util_fzf.go @@ -61,23 +61,35 @@ func fzfAliasFromAliases(ctx context.Context, aliases []*projects.ProjectAlias) return alias } -func fzfProject(ctx context.Context) (*gitlab.Project, error) { - i, err := fzf.Find(cache.Projects, fzfProjectString, - fzf.WithPreviewWindow(fzfPreviewWindow), +// If a bool=true is provided, will only allow selection of projects +// that have at least one alias defined +func fzfProject(ctx context.Context, mustHaveAlias ...bool) (*gitlab.Project, error) { + var searchableProjects []*gitlab.Project + if len(mustHaveAlias) == 1 && mustHaveAlias[0] { + searchableProjects = cache.GetProjectsWithAliases() + } else { + searchableProjects = cache.Projects + } + + i, err := fzf.Find(searchableProjects, + func(i int) string { + return searchableProjects[i].String() + }, + fzf.WithPreviewWindow( + func(i, width, height int) string { + return cache.ProjectString(searchableProjects[i]) + }, + ), fzf.WithContext(ctx), fzf.WithHeader("Fuzzy find yourself a project"), ) if err != nil || i < 0 { return nil, err } - return cache.Projects[i], nil + return searchableProjects[i], nil } func fzfPreviewWindow(i, w, h int) string { p := cache.Projects[i] return cache.ProjectString(p) } - -func fzfProjectString(i int) string { - return cache.Projects[i].String() -} diff --git a/cmd/util_init.go b/cmd/util_init.go index e8ac1eb..8df3389 100644 --- a/cmd/util_init.go +++ b/cmd/util_init.go @@ -28,10 +28,11 @@ func initProjectCache(cmd *cobra.Command, args []string) { } cacheOpts := &projects.CacheOpts{ - Path: conf.Cache.File, - TTL: conf.Cache.Ttl, - Logger: plog, - Gitlab: gitlabClient, + ProjectsPath: conf.ProjectPath, + Path: conf.Cache.File, + TTL: conf.Cache.Ttl, + Logger: plog, + Gitlab: gitlabClient, } if cache, err = projects.NewProjectCache(cacheOpts); err != nil { plog.Error("Failed to prepare project cache", plog.Args("error", err)) diff --git a/internal/gitlab/gitlab.go b/internal/gitlab/gitlab.go index 5394adb..ab6e710 100644 --- a/internal/gitlab/gitlab.go +++ b/internal/gitlab/gitlab.go @@ -3,6 +3,7 @@ package gitlab import ( "context" "fmt" + "strings" "time" "github.com/xanzy/go-gitlab" @@ -55,6 +56,10 @@ func (p *Project) String() string { return fmt.Sprintf("%s (%s)", p.Path, p.PathWithNamespace) } +func (p *Project) SanitizedPath() string { + return strings.Trim(p.PathWithNamespace, " '\"%<>|`") +} + // Given there may be thousands of projects, this will return // channels that stream progress info and then finally the full // list of projects on separate channels diff --git a/internal/projects/cache.go b/internal/projects/cache.go index 4e0ec4f..20d6a0e 100644 --- a/internal/projects/cache.go +++ b/internal/projects/cache.go @@ -22,13 +22,15 @@ type Cache struct { file string log *pterm.Logger gitlab *gitlab.Client + path string } type CacheOpts struct { - Path string - TTL time.Duration - Logger *pterm.Logger - Gitlab *gitlab.Client + Path string + ProjectsPath string + TTL time.Duration + Logger *pterm.Logger + Gitlab *gitlab.Client } // Load cache, if already loaded and up to date, nothing is done. @@ -102,7 +104,7 @@ func (c *Cache) Clear(clearAliases bool) { } func (c *Cache) refresh() { - c.log.Info("Refreshing project cache, this may take a while") + c.log.Info("Loading project cache, this may take a while\n") defer c.setUpdated() c.LoadProjects() } @@ -172,6 +174,7 @@ func NewProjectCache(opts *CacheOpts) (*Cache, error) { lock: &sync.Mutex{}, log: opts.Logger, gitlab: opts.Gitlab, + path: opts.ProjectsPath, } return cache, err diff --git a/internal/projects/cache_aliases.go b/internal/projects/cache_aliases.go index 96d0f93..6a43792 100644 --- a/internal/projects/cache_aliases.go +++ b/internal/projects/cache_aliases.go @@ -1,6 +1,25 @@ package projects -import "errors" +import ( + "errors" + + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" + "golang.org/x/exp/slices" +) + +func (c *Cache) deleteAlias(alias *ProjectAlias) { + for i, a := range c.Aliases { + if a.Alias == alias.Alias { + c.Aliases = append(c.Aliases[:i], c.Aliases[i+1:]...) + } + } +} + +func (c *Cache) DeleteAlias(alias *ProjectAlias) { + c.lock.Lock() + defer c.lock.Unlock() + c.deleteAlias(alias) +} func (c *Cache) addAlias(alias string, projectID int) error { if c.GetAliasByName(alias) != nil { @@ -21,3 +40,24 @@ func (c *Cache) AddAlias(alias string, projectID int) error { defer c.lock.Unlock() return c.addAlias(alias, projectID) } + +func (c *Cache) GetProjectsWithAliases() []*gitlab.Project { + projectList := make([]*gitlab.Project, 0) + projectsFound := make([]int, 0) + for _, a := range c.Aliases { + if !slices.Contains(projectsFound, a.ProjectID) { + projectList = append(projectList, c.GetProjectByAlias(a)) + projectsFound = append(projectsFound, a.ProjectID) + } + } + return projectList +} + +func (c *Cache) GetProjectAliasStrings(project *gitlab.Project) []string { + aliases := c.GetProjectAliases(project) + strings := make([]string, len(aliases)) + for i, a := range c.GetProjectAliases(project) { + strings[i] = a.Alias + } + return strings +} diff --git a/internal/projects/fuzz.go b/internal/projects/fuzz.go index b0ffb9f..1e90e36 100644 --- a/internal/projects/fuzz.go +++ b/internal/projects/fuzz.go @@ -24,7 +24,7 @@ func (c *Cache) FuzzyFindAlias(name string) []*ProjectAlias { found[i] = r.Target } c.log.Warn("Fuzzy found multiple aliases, try being more specific", - c.log.Args("foundAliases", strings.Join(found, ","))) + c.log.Args("foundAliases", strings.Join(found, ", "))) } var aliases []*ProjectAlias if ranks.Len() > 0 { diff --git a/internal/projects/projects.go b/internal/projects/projects.go index eabb6e6..2aaf173 100644 --- a/internal/projects/projects.go +++ b/internal/projects/projects.go @@ -1,8 +1,10 @@ package projects import ( + "bytes" "fmt" "strings" + "text/tabwriter" "github.com/pterm/pterm" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" @@ -21,10 +23,29 @@ func ProjectAliasesString(aliases []*ProjectAlias) string { return strings.Trim(str, " ") } +func (c *Cache) AliasesByProjectString() string { + var str bytes.Buffer + + w := new(tabwriter.Writer) + w.Init(&str, 10, 0, 0, ' ', tabwriter.AlignRight) + + for _, p := range c.GetProjectsWithAliases() { + var pa string + pa += pterm.LightBlue("- ") + pa += fmt.Sprint(pterm.Bold.Sprint(p.String()) + " \t ") + pa += fmt.Sprint(ProjectAliasesString(c.GetProjectAliases(p))) + fmt.Fprintln(w, pa) + } + + w.Flush() + return str.String() +} + func (c *Cache) ProjectString(p *gitlab.Project) string { info := strings.Builder{} - info.WriteString(pterm.LightBlue(p.Name)) + info.WriteString(pterm.LightGreen("\n--------------\n")) + info.WriteString(pterm.Bold.Sprint(p.Name)) info.WriteRune('\n') if p.Description != "" { info.WriteString(p.Description) @@ -40,6 +61,7 @@ func (c *Cache) ProjectString(p *gitlab.Project) string { aliases := c.GetProjectAliases(p) info.WriteString(ProjectAliasesString(aliases)) + info.WriteString(pterm.LightGreen("\n--------------\n")) return info.String() } @@ -107,7 +129,7 @@ func (c *Cache) LoadProjects() { WithShowPercentage(true). WithTotal(-1). WithTitle("Listing GitLab Projects"). - WithMaxWidth(0) + WithMaxWidth(100) defer pBar.Stop() @@ -133,6 +155,7 @@ func (c *Cache) LoadProjects() { return case <-progressInfo.DoneChan: pBar.Add(pBar.Total - curProjects) + fmt.Println("") c.log.Info("Project load complete") return } diff --git a/internal/projects/projects_fs.go b/internal/projects/projects_fs.go new file mode 100644 index 0000000..8718a99 --- /dev/null +++ b/internal/projects/projects_fs.go @@ -0,0 +1,30 @@ +package projects + +import ( + "os" + + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" +) + +func (c *Cache) GoTo(project *gitlab.Project) { + pPath := c.path + "/" + project.SanitizedPath() + + c.log.Debug("Going to project", c.log.Args( + "project", project.String(), + "path", pPath, + )) + + if _, err := os.Stat(pPath); err != nil { + c.log.Info("Preparing project path") + c.PrepProjectPath(pPath) + } +} + +func (c *Cache) PrepProjectPath(path string) { + if err := os.MkdirAll(path, 0750); err != nil { + c.log.Fatal("Failed to prepare project path", c.log.Args( + "path", path, + "error", err, + )) + } +}