diff --git a/cmd/projects/list.go b/cmd/projects/list.go index 9143631..fdf199e 100644 --- a/cmd/projects/list.go +++ b/cmd/projects/list.go @@ -3,6 +3,7 @@ package projects import ( "fmt" "os" + "strings" "text/tabwriter" "github.com/flant/glaball/pkg/client" @@ -48,6 +49,31 @@ similarity (introduced in GitLab 14.1) is only available when searching and is l return cmd } +func NewLanguagesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "languages", + Short: "List projects with languages.", + RunE: func(cmd *cobra.Command, args []string) error { + return ListWithLanguages() + }, + } + + cmd.Flags().Var(util.NewEnumValue(&groupBy, "name", "path"), "group_by", + "Return projects grouped by id, name, path, fields.") + + cmd.Flags().Var(util.NewEnumValue(&sortBy, "asc", "desc"), "sort", + "Return projects sorted in asc or desc order. Default is desc") + + cmd.Flags().StringSliceVar(&orderBy, "order_by", []string{"count", projectWithLanguagesDefaultField}, + `Return projects ordered by id, name, path, created_at, updated_at, last_activity_at, or similarity fields. +repository_size, storage_size, packages_size or wiki_size fields are only allowed for administrators. +similarity (introduced in GitLab 14.1) is only available when searching and is limited to projects that the current user is a member of.`) + + listProjectsOptionsFlags(cmd, &listProjectsOptions) + + return cmd +} + func listProjectsOptionsFlags(cmd *cobra.Command, opt *gitlab.ListProjectsOptions) { cmd.Flags().Var(util.NewBoolPtrValue(&opt.Archived), "archived", "Limit by archived status. (--archived or --no-archived). Default nil") @@ -165,6 +191,123 @@ func List() error { return nil } +func ListWithLanguages() error { + structT := new(ProjectWithLanguages) + if !sort.ValidOrderBy(orderBy, structT) { + orderBy = append(orderBy, projectWithLanguagesDefaultField) + } + + wg := common.Limiter + data := make(chan interface{}) + + for _, h := range common.Client.Hosts { + fmt.Printf("Fetching projects from %s ...\n", h.URL) + wg.Add(1) + go listProjects(h, listProjectsOptions, wg, data, common.Client.WithCache()) + } + + go func() { + wg.Wait() + close(data) + }() + + projectList := make(sort.Elements, 0) + for e := range data { + projectList = append(projectList, e) + } + + if len(projectList) == 0 { + return fmt.Errorf("no projects found") + } + + projectsWithLanguages := make(chan interface{}) + for _, v := range projectList.Typed() { + wg.Add(1) + go getProjectLanguages(v.Host, v.Struct.(*gitlab.Project), wg, projectsWithLanguages, common.Client.WithCache()) + } + + go func() { + wg.Wait() + close(projectsWithLanguages) + }() + + results, err := sort.FromChannel(projectsWithLanguages, &sort.Options{ + OrderBy: orderBy, + SortBy: sortBy, + GroupBy: groupBy, + StructType: structT, + }) + if err != nil { + return err + } + + projectsWithLanguagesFormat := util.Dict{ + { + Key: "COUNT", + Value: "[%d]", + }, + { + Key: "REPOSITORY", + Value: "%s", + }, + { + Key: "LANGUAGES", + Value: "[%s]", + }, + { + Key: "HOST", + Value: "[%s]", + }, + { + Key: "CACHED", + Value: "[%s]", + }, + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) + if _, err := fmt.Fprintln(w, strings.Join(projectsWithLanguagesFormat.Keys(), "\t")); err != nil { + return err + } + unique := 0 + total := 0 + + for _, r := range results { + unique++ // todo + total += r.Count //todo + + for _, v := range r.Elements.Typed() { + p, ok := v.Struct.(*ProjectWithLanguages) + if !ok { + return fmt.Errorf("unexpected data type: %#v", v.Struct) + } + + if err := projectsWithLanguagesFormat.Print(w, "\t", + r.Count, + r.Key, + p.LanguagesNames(), + v.Host.ProjectName(), + r.Cached, + ); err != nil { + return err + } + } + } + + if err := totalFormat.Print(w, "\n", unique, total, len(wg.Errors())); err != nil { + return err + } + + if err := w.Flush(); err != nil { + return err + } + + for _, err := range wg.Errors() { + hclog.L().Error(err.Err.Error()) + } + + return nil +} + func listProjects(h *client.Host, opt gitlab.ListProjectsOptions, wg *limiter.Limiter, data chan<- interface{}, options ...gitlab.RequestOptionFunc) error { @@ -193,3 +336,44 @@ func listProjects(h *client.Host, opt gitlab.ListProjectsOptions, wg *limiter.Li return nil } + +func getProjectLanguages(h *client.Host, project *gitlab.Project, wg *limiter.Limiter, + data chan<- interface{}, options ...gitlab.RequestOptionFunc) error { + + defer wg.Done() + + wg.Lock() + list, resp, err := h.Client.Projects.GetProjectLanguages(project.ID, options...) + wg.Unlock() + if err != nil { + wg.Error(h, err) + return err + } + + data <- sort.Element{ + Host: h, + Struct: &ProjectWithLanguages{ + Project: project, + Languages: list}, + Cached: resp.Header.Get("X-From-Cache") == "1"} + + return nil +} + +type ProjectWithLanguages struct { + Project *gitlab.Project `json:"project,omitempty"` + Languages *gitlab.ProjectLanguages `json:"languages,omitempty"` +} + +func (p ProjectWithLanguages) LanguagesNames() string { + if p.Languages == nil || len(*p.Languages) == 0 { + return "-" + } + + keys := make([]string, 0, len(*p.Languages)) + for k := range *p.Languages { + keys = append(keys, k) + } + + return strings.Join(keys, ", ") +} diff --git a/cmd/projects/projects.go b/cmd/projects/projects.go index 5b62daa..c023381 100644 --- a/cmd/projects/projects.go +++ b/cmd/projects/projects.go @@ -5,7 +5,8 @@ import ( ) const ( - projectDefaultField = "web_url" + projectDefaultField = "web_url" + projectWithLanguagesDefaultField = "project.web_url" ) func NewCmd() *cobra.Command { @@ -21,6 +22,7 @@ func NewCmd() *cobra.Command { NewMergeRequestsCmd(), NewBranchesCmd(), NewRegistryCmd(), + NewLanguagesCmd(), ) return cmd