This commit is contained in:
2023-12-08 16:52:26 -05:00
parent 424e572fe0
commit f17ce69ef8
23 changed files with 648 additions and 104 deletions

View File

@ -1,57 +1,18 @@
/*
Copyright © 2023 Ryan McGuire <ryand_mcguire@sweetwater.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// aliasCmd represents the alias command
var aliasCmd = &cobra.Command{
Use: "alias",
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("alias called")
},
Short: "Manage project aliases",
Long: aliasCmdLong,
// Just re-use the hooks for project
PersistentPreRun: initProjectCmd,
PersistentPostRun: postProjectCmd,
}
func init() {
rootCmd.AddCommand(aliasCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// aliasCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// aliasCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

95
cmd/alias_add.go Normal file
View File

@ -0,0 +1,95 @@
package cmd
import (
"fmt"
"strings"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"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/projects"
)
var aliasAddCmd = &cobra.Command{
Use: "add",
Short: "Add a project alias",
Long: aliasAddCmdLong,
Run: runAddAliasCmd,
}
func runAddAliasCmd(cmd *cobra.Command, args []string) {
var project *gitlab.Project
// Check by flag
if projectID := viper.GetInt("alias.add.projectid"); projectID > 0 {
plog.Debug(fmt.Sprintf("Adding for inbound project ID %d", projectID))
project = cache.GetProjectByID(projectID)
}
// Collect by fzf
if project == nil {
var err error
project, err = fzfProject(cmd.Context())
if err != nil || project == nil {
plog.Fatal("No project to alias, nothing to do", plog.Args("error", err))
}
}
addNewAliases(project.ID)
}
func addNewAliases(projectID int) {
project := cache.GetProjectByID(projectID)
if project == nil {
plog.Error("Failed to find project to alias", plog.Args("projectID", projectID))
return
}
// Collect the aliases
aliases := promptAliasesForProject(project)
// Add aliases
for _, a := range aliases {
a = strings.Trim(a, " '\"%")
if a == "" {
continue
}
if err := cache.AddAlias(a, project.ID); err != nil {
plog.Debug("Skipping alias add", plog.Args(
"error", err,
"alias", a,
))
} else {
plog.Info("Successfully added alias to project", plog.Args(
"project", project.String(),
"alias", a,
))
}
}
}
func promptAliasesForProject(p *gitlab.Project) []string {
aliases := cache.GetProjectAliases(p)
if len(aliases) > 0 {
plog.Info("Adding aliases to project", plog.Args(
"project", p.String(),
"existingAliases", projects.ProjectAliasesString(aliases),
))
} else {
pterm.Info.Printfln("Adding aliases to %s", p.Name)
}
response, _ := pterm.DefaultInteractiveTextInput.
WithMultiLine(false).
WithDefaultValue(p.Path + " ").
Show("Enter aliases separated by space")
return strings.Split(response, " ")
}
func init() {
aliasCmd.AddCommand(aliasAddCmd)
aliasAddCmd.PersistentFlags().Int("projectID", 0, "Specify a project by ID")
viper.BindPFlag("alias.add.projectID", aliasAddCmd.Flag("projectID"))
}

95
cmd/alias_delete.go Normal file
View File

@ -0,0 +1,95 @@
package cmd
import (
"fmt"
"strings"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"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/projects"
)
var aliasAddCmd = &cobra.Command{
Use: "add",
Short: "Add a project alias",
Long: aliasAddCmdLong,
Run: runAddAliasCmd,
}
func runAddAliasCmd(cmd *cobra.Command, args []string) {
var project *gitlab.Project
// Check by flag
if projectID := viper.GetInt("alias.add.projectid"); projectID > 0 {
plog.Debug(fmt.Sprintf("Adding for inbound project ID %d", projectID))
project = cache.GetProjectByID(projectID)
}
// Collect by fzf
if project == nil {
var err error
project, err = fzfProject(cmd.Context())
if err != nil || project == nil {
plog.Fatal("No project to alias, nothing to do", plog.Args("error", err))
}
}
addNewAliases(project.ID)
}
func addNewAliases(projectID int) {
project := cache.GetProjectByID(projectID)
if project == nil {
plog.Error("Failed to find project to alias", plog.Args("projectID", projectID))
return
}
// Collect the aliases
aliases := promptAliasesForProject(project)
// Add aliases
for _, a := range aliases {
a = strings.Trim(a, " '\"%")
if a == "" {
continue
}
if err := cache.AddAlias(a, project.ID); err != nil {
plog.Debug("Skipping alias add", plog.Args(
"error", err,
"alias", a,
))
} else {
plog.Info("Successfully added alias to project", plog.Args(
"project", project.String(),
"alias", a,
))
}
}
}
func promptAliasesForProject(p *gitlab.Project) []string {
aliases := cache.GetProjectAliases(p)
if len(aliases) > 0 {
plog.Info("Adding aliases to project", plog.Args(
"project", p.String(),
"existingAliases", projects.ProjectAliasesString(aliases),
))
} else {
pterm.Info.Printfln("Adding aliases to %s", p.Name)
}
response, _ := pterm.DefaultInteractiveTextInput.
WithMultiLine(false).
WithDefaultValue(p.Path + " ").
Show("Enter aliases separated by space")
return strings.Split(response, " ")
}
func init() {
aliasCmd.AddCommand(aliasAddCmd)
aliasAddCmd.PersistentFlags().Int("projectID", 0, "Specify a project by ID")
viper.BindPFlag("alias.add.projectID", aliasAddCmd.Flag("projectID"))
}

View File

@ -8,16 +8,12 @@ import (
"gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/projects"
)
const desc = `Contains sub-commands for managing project cache.
The project cache keeps this speedy, without smashing against the GitLab
API every time a new project is added / searched for`
var cache *projects.Cache
var cacheCmd = &cobra.Command{
Use: "cache",
Short: "Manage GitLab project cache",
Long: desc,
Long: cacheCmdLong,
PersistentPreRun: initCacheCmd,
PersistentPostRun: postCacheCmd,
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var dumpCmd = &cobra.Command{
@ -11,10 +12,16 @@ var dumpCmd = &cobra.Command{
Short: "Dump GitLab project cache",
Long: `Dumps cache to display`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(cache.String())
if conf.Dump.Full {
fmt.Println(cache.DumpString())
} else {
plog.Info(cache.String())
}
},
}
func init() {
cacheCmd.AddCommand(dumpCmd)
dumpCmd.PersistentFlags().BoolP("full", "f", false, "Dumps entire cache")
viper.BindPFlag("dump.full", dumpCmd.LocalFlags().Lookup("full"))
}

48
cmd/project.go Normal file
View File

@ -0,0 +1,48 @@
package cmd
import (
"github.com/spf13/cobra"
"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",
Args: cobra.MaximumNArgs(1),
ArgAliases: []string{"alias"},
ValidArgsFunction: validAliasesFunc,
Long: projCmdLong,
PersistentPreRun: initProjectCmd,
PersistentPostRun: postProjectCmd,
Run: projectCmdRun,
}
func projectCmdRun(cmd *cobra.Command, args []string) {
var searchString string
if len(args) > 0 {
searchString = args[0]
}
project := fzfFindProject(searchString)
if project == nil {
plog.Fatal("Failed to find a project, nothing to do")
} else {
plog.Info("Houston, we have a project", plog.Args(
"project", project.String(),
"aliases", projects.ProjectAliasesString(
cache.GetProjectAliases(project)),
))
}
}
func initProjectCmd(cmd *cobra.Command, args []string) {
initProjectCache(cmd, args)
}
func postProjectCmd(cmd *cobra.Command, args []string) {
postProjectCache(cmd, args)
}
func init() {
rootCmd.AddCommand(projectCmd)
}

44
cmd/project_show.go Normal file
View File

@ -0,0 +1,44 @@
package cmd
import (
"fmt"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
var projectShowCmd = &cobra.Command{
Use: "show [fuzzy alias search]",
Short: "Show detail for a GitLab project",
Args: cobra.ArbitraryArgs,
ValidArgsFunction: validProjectsOrAliasesFunc,
Long: projShowCmdLong,
Run: projectShowCmdRun,
}
func projectShowCmdRun(cmd *cobra.Command, args []string) {
var searchString string
if len(args) > 0 {
searchString = args[0]
}
project := fzfFindProject(searchString)
if project == nil {
var err error
project, err = fzfProject(cmd.Context())
if err != nil || project == nil {
plog.Fatal("Failed to find project, nothing to show", plog.Args(
"error", err,
))
}
}
fmt.Println(pterm.LightGreen("\n--------------"))
fmt.Println(cache.ProjectString(project))
fmt.Println(pterm.LightGreen("--------------\n"))
}
func init() {
projectCmd.AddCommand(projectShowCmd)
}

View File

@ -12,21 +12,13 @@ import (
"gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/config"
)
const (
defGitlabHost = "gitlab.sweetwater.com"
defProjectsPath = "~/work/projects"
defLogLevel = "info"
)
var conf config.Config
var plog *pterm.Logger
var rootCmd = &cobra.Command{
Use: "gitlab-project-manager",
Short: "Find and use GitLab projects locally",
Long: `Finds GitLab projects using fuzzy-find, remembering
your chosen term for the project as an alias, and offers helpful
shortcuts for moving around in projects and opening your code`,
Use: "gitlab-project-manager",
Short: "Find and use GitLab projects locally",
Long: rootCmdLong,
PersistentPreRun: initRootCmd,
}

20
cmd/util_completion.go Normal file
View File

@ -0,0 +1,20 @@
package cmd
import "github.com/spf13/cobra"
func validProjectsFunc(cmd *cobra.Command, args []string, toComplete string) (
[]string, cobra.ShellCompDirective) {
return cache.ProjectStrings(), cobra.ShellCompDirectiveDefault
}
func validAliasesFunc(cmd *cobra.Command, args []string, toComplete string) (
[]string, cobra.ShellCompDirective) {
return cache.AliasStrings(), cobra.ShellCompDirectiveDefault
}
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
}

28
cmd/util_constants.go Normal file
View File

@ -0,0 +1,28 @@
package cmd
const (
defGitlabHost = "gitlab.sweetwater.com"
defProjectsPath = "~/work/projects"
defLogLevel = "info"
)
const aliasCmdLong = `Manages project aliases, with options for
listing, adding, and deleting.`
const aliasAddCmdLong = `Adds a project alias to a project
project ID can be provided, or will otherwise use fuzzy find`
const cacheCmdLong = `Contains sub-commands for managing project cache.
The project cache keeps this speedy, without smashing against the GitLab
API every time a new project is added / searched for`
const rootCmdLong = `Finds GitLab projects using fuzzy-find, remembering
your chosen term for the project as an alias, and offers helpful
shortcuts for moving around in projects and opening your code`
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 projShowCmdLong = `Shows detail for a particular project
Will always fuzzy find`

83
cmd/util_fzf.go Normal file
View File

@ -0,0 +1,83 @@
package cmd
import (
"context"
fzf "github.com/ktr0731/go-fuzzyfinder"
"gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab"
"gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/projects"
)
func fzfFindProject(searchString string) *gitlab.Project {
var project *gitlab.Project
if searchString != "" {
project = fzfSearchProjectAliases(searchString)
} else {
var err error
project, err = fzfProject(rootCmd.Context())
if project == nil || err != nil {
return nil
}
}
return project
}
func fzfSearchProjectAliases(searchString string) *gitlab.Project {
var project *gitlab.Project
var alias *projects.ProjectAlias
if alias = cache.GetAliasByName(searchString); alias != nil {
project = cache.GetProjectByAlias(alias)
plog.Info("Perfect alias match... flawless")
} else {
// Get fuzzy if we don't have an exact match
aliases := cache.FuzzyFindAlias(searchString)
if len(aliases) > 1 {
alias = fzfAliasFromAliases(aliasAddCmd.Context(), aliases)
} else if len(aliases) == 1 {
alias = aliases[0]
}
project = cache.GetProjectByAlias(alias)
}
return project
}
func fzfAliasFromAliases(ctx context.Context, aliases []*projects.ProjectAlias) *projects.ProjectAlias {
var alias *projects.ProjectAlias
i, err := fzf.Find(
aliases,
func(i int) string {
return aliases[i].Alias + " -> " + cache.GetProjectByAlias(aliases[i]).PathWithNamespace
},
fzf.WithContext(ctx),
fzf.WithHeader("Choose an Alias"),
)
if err != nil {
plog.Error("Failed to fzf alias slice", plog.Args("error", err))
} else {
alias = aliases[i]
}
return alias
}
func fzfProject(ctx context.Context) (*gitlab.Project, error) {
i, err := fzf.Find(cache.Projects, fzfProjectString,
fzf.WithPreviewWindow(fzfPreviewWindow),
fzf.WithContext(ctx),
fzf.WithHeader("Fuzzy find yourself a project"),
)
if err != nil || i < 0 {
return nil, err
}
return cache.Projects[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

@ -18,8 +18,8 @@ import (
// func from their PersistentPreRun commands
func initProjectCache(cmd *cobra.Command, args []string) {
plog.Debug("Running persistent pre-run for cacheCmd")
conf.Cache.File = conf.ProjectPath + "/.cache.json"
plog.Debug("Running pre-run for cacheCmd")
conf.Cache.File = conf.ProjectPath + "/.cache.yaml"
gitlabClient, err := gitlab.NewGitlabClient(cmd.Context(), conf.GitlabHost, conf.GitlabToken)
if err != nil {
@ -38,7 +38,7 @@ func initProjectCache(cmd *cobra.Command, args []string) {
os.Exit(1)
}
if err := cache.Load(); err != nil {
if err := cache.Read(); err != nil {
plog.Error("Cache load failed", plog.Args("error", err))
os.Exit(1)
}