package cmd import ( "context" fzf "github.com/ktr0731/go-fuzzyfinder" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/cache" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/projects" "golang.org/x/exp/slices" ) type fzfProjectOpts struct { Ctx context.Context Search string MustHaveAlias bool Remotes []string } // This will try to find a project by alias if a search term // is given, otherwise will fuzzy find by project func fzfFindProject(opts *fzfProjectOpts) *projects.Project { var project *projects.Project if opts.Search != "" { project = fzfSearchProjectAliases(opts) } else { var err error project, err = fzfProject(opts) if project == nil || err != nil { return nil } } return project } // If . is given as a project, will open project from the // current working directory. Otherwise, will attempt to fuzzy-find // a project given a search term if provided func fzfCwdOrSearchProjectAliases(opts *fzfProjectOpts) *projects.Project { var project *projects.Project if opts.Search == "." { project, _ = projectCache.GetProjectFromCwd() } else { project = fzfSearchProjectAliases(opts) } return project } // This will fuzzy search only aliases, preferring an exact // match if one is given func fzfSearchProjectAliases(opts *fzfProjectOpts) *projects.Project { var project *projects.Project var alias *cache.ProjectAlias if alias = projectCache.GetAliasByName(opts.Search, opts.Remotes...); alias != nil { project = projectCache.GetProjectByAlias(alias) plog.Info("Perfect alias match... flawless") } else { // Get fuzzy if we don't have an exact match aliases := projectCache.FuzzyFindAlias(opts.Search) if len(aliases) > 1 { // If multiple aliases were found, switch over to project // by alias mode with merging // alias = fzfAliasFromAliases(rootCmd.Context(), aliases) project, _ = fzfProjectFromAliases(opts, aliases) } else if len(aliases) == 1 { alias = aliases[0] project = projectCache.GetProjectByAlias(alias) } } return project } // Given a list of aliases, will fuzzy-find and return // a single one. Replaced by fzfProjectFromAliases in fzfSearchProjectAliases // as merging is preferred, but can be used if it's ever desirable to // return a single alias from all aliases func fzfAliasFromAliases(opts *fzfProjectOpts, aliases []*cache.ProjectAlias) *cache.ProjectAlias { var alias *cache.ProjectAlias i, err := fzf.Find( aliases, func(i int) string { return aliases[i].Alias + " -> " + projectCache.GetProjectByAlias(aliases[i]).PathWithNamespace }, fzf.WithContext(opts.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 } // Given a list of aliases, merge them together and use the resulting // list of projects to return a project func fzfProjectFromAliases(opts *fzfProjectOpts, aliases []*cache.ProjectAlias) ( *projects.Project, error) { mergedProjects := projectsFromAliases(aliases) if len(mergedProjects) == 1 { return mergedProjects[0], nil } return fzfProjectFromProjects(opts, mergedProjects) } func projectsFromAliases(aliases []*cache.ProjectAlias) []*projects.Project { projects := make([]*projects.Project, 0) ALIASES: for _, a := range aliases { for _, p := range projects { // Already have it if a.ProjectID == p.ID && a.Remote == p.Remote { continue ALIASES } } projects = append(projects, projectCache.GetProjectByAlias(a)) } return projects } // If opts.MustHaveAlias, will only allow selection of projects // that have at least one alias defined func fzfProject(opts *fzfProjectOpts) (*projects.Project, error) { var searchableProjects []*projects.Project if opts.MustHaveAlias { searchableProjects = projectCache.GetProjectsWithAliases() } else { searchableProjects = projectCache.Projects } // Filter out unwanted remotes if provided searchableProjects = filterProjectsWithRemotes(searchableProjects, opts.Remotes...) return fzfProjectFromProjects(opts, searchableProjects) } // Takes a list of projects and performs a fuzzyfind func fzfProjectFromProjects(opts *fzfProjectOpts, projects []*projects.Project) ( *projects.Project, error) { i, err := fzf.Find(projects, func(i int) string { // Display the project along with its aliases return projectCache.GetProjectStringWithAliases(projects[i]) }, fzf.WithPreviewWindow( func(i, width, height int) string { return projectCache.ProjectString(projects[i]) }, ), fzf.WithContext(opts.Ctx), fzf.WithHeader("Fuzzy find yourself a project"), ) if err != nil || i < 0 { return nil, err } return projects[i], nil } func fzfPreviewWindow(i, w, h int) string { p := projectCache.Projects[i] return projectCache.ProjectString(p) } func filterProjectsWithRemotes(gitProjects []*projects.Project, remotes ...string) []*projects.Project { filteredProjects := make([]*projects.Project, 0, len(gitProjects)) if len(remotes) > 0 { for _, p := range gitProjects { if slices.Contains(remotes, p.Remote) { filteredProjects = append(filteredProjects, p) } } } else { filteredProjects = gitProjects } return filteredProjects } // Nearly useless function that simply returns either an // empty string, or a string from the first arg if one is provided func searchStringFromArgs(args []string) string { var term string if len(args) > 0 { term = args[0] } return term }