diff --git a/cmd/util_init.go b/cmd/util_init.go index 3f5625d..148a80c 100644 --- a/cmd/util_init.go +++ b/cmd/util_init.go @@ -11,6 +11,7 @@ import ( "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/config" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes" gitearemote "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/gitea" + githubremote "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/github" gitlabremote "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/gitlab" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/remote" "golang.org/x/sys/unix" @@ -65,6 +66,8 @@ func getRemotes(cmd *cobra.Command) *remotes.Remotes { gitRemote, err = gitlabremote.NewGitlabRemote(&gitRemoteInfo) case "gitea": gitRemote, err = gitearemote.NewGiteaRemote(&gitRemoteInfo) + case "github": + gitRemote, err = githubremote.NewGithubRemote(&gitRemoteInfo) } if err != nil { plog.Error("Failed to prepare remote", plog.Args( diff --git a/go.mod b/go.mod index 4bed2db..a9c588a 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-github/v58 v58.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/go.sum b/go.sum index 77e0bb7..f1e7be5 100644 --- a/go.sum +++ b/go.sum @@ -170,6 +170,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw= +github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= diff --git a/internal/remotes/github/github.go b/internal/remotes/github/github.go new file mode 100644 index 0000000..8c9917e --- /dev/null +++ b/internal/remotes/github/github.go @@ -0,0 +1,45 @@ +package githubremote + +import ( + "fmt" + + "github.com/google/go-github/v58/github" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/info" +) + +type GithubRemote struct { + user *github.User + info *info.RemoteInfo + api *github.Client +} + +func (r *GithubRemote) GetInfo() *info.RemoteInfo { + return r.info +} + +func (r *GithubRemote) GetType() string { + return r.info.Type.String() +} + +func (r *GithubRemote) String() string { + return fmt.Sprintf("Github %s (%s), clone proto %s", + r.GetInfo().Name, r.GetInfo().Host, r.GetInfo().CloneProto) +} + +func NewGithubRemote(remoteInfo *info.RemoteInfo) (*GithubRemote, error) { + client := github.NewClient(nil). + WithAuthToken(remoteInfo.Token) + + githubRemote := &GithubRemote{ + info: remoteInfo, + api: client, + } + + user, _, err := githubRemote.api.Users.Get(remoteInfo.Context(), "") + if err != nil { + return nil, err + } + githubRemote.user = user + + return githubRemote, nil +} diff --git a/internal/remotes/github/github_api.go b/internal/remotes/github/github_api.go new file mode 100644 index 0000000..97e31e3 --- /dev/null +++ b/internal/remotes/github/github_api.go @@ -0,0 +1,72 @@ +package githubremote + +import ( + "math" + + "github.com/google/go-github/v58/github" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/projects" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/remote" +) + +func (r *GithubRemote) GetNumProjects(opts *remote.RemoteQueryOpts) int { + var projects int + if opts.OwnerOnly { + projects = int(r.user.GetOwnedPrivateRepos()) + } else { + projects += int(r.user.GetTotalPrivateRepos()) + projects += r.user.GetPublicRepos() + } + return projects +} + +func (r *GithubRemote) ReposToProjects(repos []*github.Repository) []*projects.Project { + pList := make([]*projects.Project, len(repos)) + for i, repo := range repos { + var ownerName, avatar string + owner := repo.GetOwner() + if owner != nil { + ownerName = owner.GetName() + avatar = owner.GetAvatarURL() + } + // owner, name := GetOwnerRepo(repo.FullName) + project := &projects.Project{ + ID: int(repo.GetID()), + Owner: ownerName, + Description: repo.GetDescription(), + SSHURLToRepo: repo.GetSSHURL(), + HTTPURLToRepo: repo.GetCloneURL(), + WebURL: repo.GetHTMLURL(), + Name: repo.GetName(), + NameWithNamespace: repo.GetFullName(), + Path: repo.GetName(), + AvatarURL: avatar, + PathWithNamespace: repo.GetFullName(), + LastActivityAt: repo.GetPushedAt().Time, + Remote: r.info.Host, + Languages: r.GetRepoLangs(repo), + } + pList[i] = project + } + return pList +} + +func (r *GithubRemote) GetRepoLangs(repo *github.Repository) *projects.ProjectLanguages { + languages := projects.NewProjectLanguages() + langs, _, err := r.api.Repositories.ListLanguages(r.info.Context(), r.user.GetName(), repo.GetName()) + if err != nil { + var ttlLines int + for _, lines := range langs { + ttlLines += lines + } + + for l, n := range langs { + pcnt := float64(n) / float64(ttlLines) * 100 + pcnt = math.Round(pcnt*100) / 100 + languages.AddLanguage(&projects.ProjectLanguage{ + Name: l, + Percentage: float32(pcnt), + }) + } + } + return languages +} diff --git a/internal/remotes/github/github_stream.go b/internal/remotes/github/github_stream.go new file mode 100644 index 0000000..997631f --- /dev/null +++ b/internal/remotes/github/github_stream.go @@ -0,0 +1,62 @@ +package githubremote + +import ( + "errors" + + "github.com/google/go-github/v58/github" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/load" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/remote" +) + +const githubReposPerPage = 20 + +func (r *GithubRemote) StreamProjects(pi *load.ProgressInfo, opts *remote.RemoteQueryOpts) { + defer close(pi.ProgressChan) + defer close(pi.ProjectsChan) + + // Get projects. TODO support concurrency + githubListOpts := github.ListOptions{ + Page: 1, + PerPage: githubReposPerPage, + } + var githubRepoType string + if opts.OwnerOnly { + githubRepoType = "owner" + } else { + githubRepoType = "all" + } + for { + repos, resp, err := r.api.Repositories.ListByAuthenticatedUser( + r.info.Context(), + &github.RepositoryListByAuthenticatedUserOptions{ + Type: githubRepoType, + ListOptions: githubListOpts, + }, + ) + + if err != nil { + pi.ErrorChan <- err + break + } else if len(repos) < 1 { + pi.ErrorChan <- errors.New("No github repos found") + break + } + + // Write updates to channels + pi.ProjectsChan <- r.ReposToProjects(repos) + pi.ProgressChan <- load.Progress{ + Page: githubListOpts.Page, + Pages: resp.LastPage, + Projects: len(repos), + TotalProjects: pi.NumProjects, + } + + if resp.NextPage == 0 { + break + } + + githubListOpts.Page = resp.NextPage + } + + pi.DoneChan <- nil +} diff --git a/internal/remotes/info/info.go b/internal/remotes/info/info.go index 70c86b8..a8969d8 100644 --- a/internal/remotes/info/info.go +++ b/internal/remotes/info/info.go @@ -20,9 +20,11 @@ type RemoteTypes []RemoteType var ( RemoteTypeGitlab RemoteType = "gitlab" RemoteTypeGitea RemoteType = "gitea" + RemoteTypeGithub RemoteType = "github" RemoteTypesAll RemoteTypes = []RemoteType{ RemoteTypeGitea, RemoteTypeGitlab, + RemoteTypeGithub, } ) @@ -33,6 +35,8 @@ func GetRemoteTypeFromString(remoteType string) RemoteType { rt = RemoteTypeGitea case RemoteTypeGitlab.String(): rt = RemoteTypeGitlab + case RemoteTypeGithub.String(): + rt = RemoteTypeGithub } return rt }