This commit is contained in:
Ryan McGuire 2023-12-08 23:13:17 -05:00
parent 82e4f2f51e
commit 4e76c9efe1
17 changed files with 265 additions and 65 deletions

View File

@ -6,6 +6,7 @@ import (
var aliasCmd = &cobra.Command{ var aliasCmd = &cobra.Command{
Use: "alias", Use: "alias",
Aliases: []string{"aliases", "a"},
Short: "Manage project aliases", Short: "Manage project aliases",
Long: aliasCmdLong, Long: aliasCmdLong,
// Just re-use the hooks for project // Just re-use the hooks for project

View File

@ -13,6 +13,7 @@ import (
var aliasAddCmd = &cobra.Command{ var aliasAddCmd = &cobra.Command{
Use: "add", Use: "add",
Aliases: []string{"set", "a", "s"},
Short: "Add a project alias", Short: "Add a project alias",
Args: cobra.ArbitraryArgs, Args: cobra.ArbitraryArgs,
Long: aliasAddCmdLong, Long: aliasAddCmdLong,
@ -57,7 +58,7 @@ func addNewAliases(projectID int) {
// Add aliases // Add aliases
for _, a := range aliases { for _, a := range aliases {
a = strings.Trim(a, " '\"%") a = strings.Trim(a, " '\"%<>|`")
if a == "" { if a == "" {
continue continue
} }

View File

@ -3,6 +3,7 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab"
@ -10,6 +11,7 @@ import (
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"},
Short: "Delete a project alias", Short: "Delete a project alias",
Long: aliasDeleteCmdLong, Long: aliasDeleteCmdLong,
Run: runDeleteAliasCmd, Run: runDeleteAliasCmd,
@ -17,14 +19,54 @@ var aliasDeleteCmd = &cobra.Command{
func runDeleteAliasCmd(cmd *cobra.Command, args []string) { func runDeleteAliasCmd(cmd *cobra.Command, args []string) {
var project *gitlab.Project var project *gitlab.Project
var err error
if len(args) > 0 { if len(args) > 0 {
project = fzfFindProject(args[0]) project = fzfFindProject(args[0])
} else { } 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() { func init() {

24
cmd/alias_list.go Normal file
View File

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

View File

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

View File

@ -2,12 +2,14 @@ package cmd
import ( import (
"github.com/spf13/cobra" "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" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/projects"
) )
var projectCmd = &cobra.Command{ var projectCmd = &cobra.Command{
Use: "project [fuzzy alias search]", Use: "project [fuzzy alias search]",
Short: "Use a GitLab project", Short: "Use a GitLab project",
Aliases: []string{"proj", "projects", "p"},
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
ArgAliases: []string{"alias"}, ArgAliases: []string{"alias"},
ValidArgsFunction: validAliasesFunc, ValidArgsFunction: validAliasesFunc,
@ -18,10 +20,15 @@ var projectCmd = &cobra.Command{
} }
func projectCmdRun(cmd *cobra.Command, args []string) { func projectCmdRun(cmd *cobra.Command, args []string) {
goToProject(getProject(args))
}
func getProject(args []string) *gitlab.Project {
var searchString string var searchString string
if len(args) > 0 { if len(args) > 0 {
searchString = args[0] searchString = args[0]
} }
project := fzfFindProject(searchString) project := fzfFindProject(searchString)
if project == nil { if project == nil {
@ -33,6 +40,13 @@ func projectCmdRun(cmd *cobra.Command, args []string) {
cache.GetProjectAliases(project)), 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) { func initProjectCmd(cmd *cobra.Command, args []string) {

25
cmd/project_go.go Normal file
View File

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

View File

@ -3,7 +3,6 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/pterm/pterm"
"github.com/spf13/cobra" "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(cache.ProjectString(project))
fmt.Println(pterm.LightGreen("--------------\n"))
} }
func init() { func init() {

View File

@ -9,6 +9,8 @@ const (
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 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`
@ -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 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
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 const projShowCmdLong = `Shows detail for a particular project
Will always fuzzy find` Will always fuzzy find`

View File

@ -61,23 +61,35 @@ func fzfAliasFromAliases(ctx context.Context, aliases []*projects.ProjectAlias)
return alias return alias
} }
func fzfProject(ctx context.Context) (*gitlab.Project, error) { // If a bool=true is provided, will only allow selection of projects
i, err := fzf.Find(cache.Projects, fzfProjectString, // that have at least one alias defined
fzf.WithPreviewWindow(fzfPreviewWindow), 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.WithContext(ctx),
fzf.WithHeader("Fuzzy find yourself a project"), fzf.WithHeader("Fuzzy find yourself a project"),
) )
if err != nil || i < 0 { if err != nil || i < 0 {
return nil, err return nil, err
} }
return cache.Projects[i], nil return searchableProjects[i], nil
} }
func fzfPreviewWindow(i, w, h int) string { func fzfPreviewWindow(i, w, h int) string {
p := cache.Projects[i] p := cache.Projects[i]
return cache.ProjectString(p) return cache.ProjectString(p)
} }
func fzfProjectString(i int) string {
return cache.Projects[i].String()
}

View File

@ -28,6 +28,7 @@ func initProjectCache(cmd *cobra.Command, args []string) {
} }
cacheOpts := &projects.CacheOpts{ cacheOpts := &projects.CacheOpts{
ProjectsPath: conf.ProjectPath,
Path: conf.Cache.File, Path: conf.Cache.File,
TTL: conf.Cache.Ttl, TTL: conf.Cache.Ttl,
Logger: plog, Logger: plog,

View File

@ -3,6 +3,7 @@ package gitlab
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/xanzy/go-gitlab" "github.com/xanzy/go-gitlab"
@ -55,6 +56,10 @@ func (p *Project) String() string {
return fmt.Sprintf("%s (%s)", p.Path, p.PathWithNamespace) 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 // Given there may be thousands of projects, this will return
// channels that stream progress info and then finally the full // channels that stream progress info and then finally the full
// list of projects on separate channels // list of projects on separate channels

View File

@ -22,10 +22,12 @@ type Cache struct {
file string file string
log *pterm.Logger log *pterm.Logger
gitlab *gitlab.Client gitlab *gitlab.Client
path string
} }
type CacheOpts struct { type CacheOpts struct {
Path string Path string
ProjectsPath string
TTL time.Duration TTL time.Duration
Logger *pterm.Logger Logger *pterm.Logger
Gitlab *gitlab.Client Gitlab *gitlab.Client
@ -102,7 +104,7 @@ func (c *Cache) Clear(clearAliases bool) {
} }
func (c *Cache) refresh() { 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() defer c.setUpdated()
c.LoadProjects() c.LoadProjects()
} }
@ -172,6 +174,7 @@ func NewProjectCache(opts *CacheOpts) (*Cache, error) {
lock: &sync.Mutex{}, lock: &sync.Mutex{},
log: opts.Logger, log: opts.Logger,
gitlab: opts.Gitlab, gitlab: opts.Gitlab,
path: opts.ProjectsPath,
} }
return cache, err return cache, err

View File

@ -1,6 +1,25 @@
package projects 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 { func (c *Cache) addAlias(alias string, projectID int) error {
if c.GetAliasByName(alias) != nil { if c.GetAliasByName(alias) != nil {
@ -21,3 +40,24 @@ func (c *Cache) AddAlias(alias string, projectID int) error {
defer c.lock.Unlock() defer c.lock.Unlock()
return c.addAlias(alias, projectID) 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
}

View File

@ -1,8 +1,10 @@
package projects package projects
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
"text/tabwriter"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab"
@ -21,10 +23,29 @@ func ProjectAliasesString(aliases []*ProjectAlias) string {
return strings.Trim(str, " ") 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 { func (c *Cache) ProjectString(p *gitlab.Project) string {
info := strings.Builder{} 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') info.WriteRune('\n')
if p.Description != "" { if p.Description != "" {
info.WriteString(p.Description) info.WriteString(p.Description)
@ -40,6 +61,7 @@ func (c *Cache) ProjectString(p *gitlab.Project) string {
aliases := c.GetProjectAliases(p) aliases := c.GetProjectAliases(p)
info.WriteString(ProjectAliasesString(aliases)) info.WriteString(ProjectAliasesString(aliases))
info.WriteString(pterm.LightGreen("\n--------------\n"))
return info.String() return info.String()
} }
@ -107,7 +129,7 @@ func (c *Cache) LoadProjects() {
WithShowPercentage(true). WithShowPercentage(true).
WithTotal(-1). WithTotal(-1).
WithTitle("Listing GitLab Projects"). WithTitle("Listing GitLab Projects").
WithMaxWidth(0) WithMaxWidth(100)
defer pBar.Stop() defer pBar.Stop()
@ -133,6 +155,7 @@ func (c *Cache) LoadProjects() {
return return
case <-progressInfo.DoneChan: case <-progressInfo.DoneChan:
pBar.Add(pBar.Total - curProjects) pBar.Add(pBar.Total - curProjects)
fmt.Println("")
c.log.Info("Project load complete") c.log.Info("Project load complete")
return return
} }

View File

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