wip
This commit is contained in:
parent
82e4f2f51e
commit
4e76c9efe1
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
24
cmd/alias_list.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
25
cmd/project_go.go
Normal 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)
|
||||
}
|
@ -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() {
|
||||
|
@ -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`
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
30
internal/projects/projects_fs.go
Normal file
30
internal/projects/projects_fs.go
Normal 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,
|
||||
))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user