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{
Use: "alias",
Aliases: []string{"aliases", "a"},
Short: "Manage project aliases",
Long: aliasCmdLong,
// Just re-use the hooks for project

View File

@ -13,6 +13,7 @@ import (
var aliasAddCmd = &cobra.Command{
Use: "add",
Aliases: []string{"set", "a", "s"},
Short: "Add a project alias",
Args: cobra.ArbitraryArgs,
Long: aliasAddCmdLong,
@ -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
}

View File

@ -3,6 +3,7 @@ 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"
@ -10,6 +11,7 @@ import (
var aliasDeleteCmd = &cobra.Command{
Use: "delete [fuzzy project or alias]",
Aliases: []string{"rm", "del", "d"},
Short: "Delete a project alias",
Long: aliasDeleteCmdLong,
Run: runDeleteAliasCmd,
@ -17,14 +19,54 @@ var aliasDeleteCmd = &cobra.Command{
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() {

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 (
"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) {

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 (
"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() {

View File

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

View File

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

View File

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

View File

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

View File

@ -22,10 +22,12 @@ type Cache struct {
file string
log *pterm.Logger
gitlab *gitlab.Client
path string
}
type CacheOpts struct {
Path string
ProjectsPath string
TTL time.Duration
Logger *pterm.Logger
Gitlab *gitlab.Client
@ -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

View File

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

View File

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

View File

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

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