package gitlab import ( "context" "fmt" "strings" "time" "github.com/go-git/go-git/v5" "github.com/pterm/pterm" "github.com/xanzy/go-gitlab" ) const defProjectsPerPage = 30 type Client struct { Ctx context.Context gitlab *gitlab.Client } type Project struct { ID int Description string SSHURLToRepo string HTTPURLToRepo string WebURL string Name string NameWithNamespace string Path string PathWithNamespace string AvatarURL string LastActivityAt time.Time Readme string Languages *ProjectLanguages gitRepo *git.Repository } type ProjectLanguages []*ProjectLanguage type ProjectLanguage struct { Name string Percentage float32 } type User struct { ID int Username string Email string Name string AvatarURL string } type ProgressInfo struct { ProgressChan chan Progress ProjectsChan chan []*Project ErrorChan chan error DoneChan chan interface{} } type Progress struct { Page int Pages int Projects int TotalProjects int } func (p *Project) String() string { return fmt.Sprintf("%s (%s)", p.Path, p.PathWithNamespace) } func (p *Project) GetLanguage() *ProjectLanguage { if p.Languages == nil { return nil } var lang *ProjectLanguage var maxPcnt float32 for _, p := range *p.Languages { if p.Percentage > maxPcnt { lang = p } maxPcnt = p.Percentage } return lang } func (p *Project) SanitizedPath() string { return strings.Trim(p.PathWithNamespace, " '\"%<>|`") } func (p *Project) SetRepo(r *git.Repository) { p.gitRepo = r } func (p *Project) GetRepo() *git.Repository { return p.gitRepo } func (p *Project) GetGitInfo() string { repo := p.GetRepo() if repo == nil { return "No Repo" } var str string str += "\n" + pterm.LightRed("Project: ") + pterm.Bold.Sprint(p.Name) + "\n" head, _ := repo.Head() branch := head.Name().String()[11:] b, _ := repo.Branch(branch) if b != nil { str += pterm.LightCyan("Branch: ") + pterm.Bold.Sprint(b.Name) + "\n" } else { str += pterm.LightCyan("NEW Branch: ") + pterm.Bold.Sprint(branch) + "\n" } commit, _ := repo.CommitObject(head.Hash()) str += "\n" + commit.String() str += pterm.LightMagenta("GitLab: ") + pterm.Bold.Sprint(p.HTTPURLToRepo) + "\n" if remotes, _ := repo.Remotes(); len(remotes) > 0 { str += pterm.LightBlue("Remote: ") + pterm.Bold.Sprint(remotes[0].Config().URLs[0]) } return pterm.DefaultBox. WithLeftPadding(5).WithRightPadding(5). WithBoxStyle(&pterm.Style{pterm.FgLightBlue}). WithTitle(pterm.Bold.Sprint(pterm.LightGreen("Project Git Status"))). Sprint(str) } // 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. If ownerOnly=true, only // projects for which you are an owner will be loaded func (c *Client) StreamProjects(ownerOnly bool) *ProgressInfo { pi := &ProgressInfo{ ProgressChan: make(chan Progress), ProjectsChan: make(chan []*Project), ErrorChan: make(chan error), DoneChan: make(chan interface{}), } go c.streamProjects(pi, ownerOnly) return pi } func (c *Client) streamProjects(pi *ProgressInfo, ownerOnly bool) { defer close(pi.ProgressChan) defer close(pi.ProjectsChan) listOpts := &gitlab.ListProjectsOptions{ ListOptions: gitlab.ListOptions{ PerPage: defProjectsPerPage, Page: 1, }, Archived: gitlab.Ptr[bool](false), Owned: gitlab.Ptr[bool](ownerOnly), } var numProjects int for { projects, resp, err := c.ListProjects(listOpts) if err != nil { pi.ErrorChan <- err break } // We're done when we have it all or our context is done if c.Ctx.Err() != nil || resp.NextPage == 0 { pi.DoneChan <- nil break } numProjects += len(projects) pi.ProjectsChan <- projects pi.ProgressChan <- Progress{ Page: resp.CurrentPage, Pages: resp.TotalPages, Projects: numProjects, TotalProjects: resp.TotalItems, } listOpts.Page = resp.NextPage } } // Returns a list of projects along with the next page and an error // if there was an error func (c *Client) ListProjects(opts *gitlab.ListProjectsOptions) ( []*Project, *gitlab.Response, error) { pList := make([]*Project, 0) projects, resp, err := c.gitlab.Projects.ListProjects( opts, gitlab.WithContext(c.Ctx), ) if err == nil { pList = append(pList, c.handleProjects(projects)...) } return pList, resp, err } func (c *Client) handleProjects(projects []*gitlab.Project) []*Project { // Opportunity to perform any filtering or additional lookups // on a per-project basis pList := make([]*Project, 0, len(projects)) for _, project := range projects { p := &Project{ ID: project.ID, Description: project.Description, SSHURLToRepo: project.SSHURLToRepo, HTTPURLToRepo: project.HTTPURLToRepo, WebURL: project.WebURL, Name: project.Name, NameWithNamespace: project.NameWithNamespace, Path: project.Path, PathWithNamespace: project.PathWithNamespace, AvatarURL: project.AvatarURL, LastActivityAt: *project.LastActivityAt, Readme: project.ReadmeURL, Languages: c.GetProjectLanguages(project), } pList = append(pList, p) } return pList } // A nil return indicates an API error or GitLab doesn't know what // language the project uses. func (c *Client) GetProjectLanguages(project *gitlab.Project) *ProjectLanguages { l, _, e := c.gitlab.Projects.GetProjectLanguages(project.ID, gitlab.WithContext(c.Ctx)) if e != nil { pterm.Error.Printfln("Failed requesting project languages: %s", e.Error()) return nil } var pLangs ProjectLanguages pLangs = make([]*ProjectLanguage, len(*l)) var i int for name, pcnt := range *l { pLangs[i] = &ProjectLanguage{ Name: name, Percentage: pcnt, } i++ } return &pLangs } func NewGitlabClient(ctx context.Context, host, token string) (*Client, error) { client, err := gitlab.NewClient(token, gitlab.WithBaseURL(host)) if err != nil { return nil, err } gitlabClient := &Client{ Ctx: ctx, gitlab: client, } return gitlabClient, nil }