From 7acace3560d32ba71630bec2fce9d55b1dcc671b Mon Sep 17 00:00:00 2001 From: Michael Gebetsroither Date: Wed, 14 Oct 2026 12:10:08 +0200 Subject: [PATCH] Add organization option to back up public repos of an org/group Lists all public repositories of the given GitHub organizations or GitLab groups (including subgroups), also when not a member. Results are merged with the other discovery mechanisms and filtered by exclude. Co-Authored-By: Claude Opus 5.5 --- README.md | 14 ++++++++++++++ github.go | 26 ++++++++++++++++++++++++++ gitlab.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/README.md b/README.md index 1c993bb..5d68d5a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,13 @@ github: # (default: []) repositories: - ChappIO/git-backup + # (optional) Backup all public repositories of these + # organizations, even if you are not a member. + # Will be merged with the other discovery mechanisms + # and it's result filtered with excluded repos. + # (default: []) + organization: + - golang # (optional) Exclude this list of repos # or whole organizations/users exclude: @@ -85,6 +92,13 @@ gitlab: repositories: - gnuwget/wget2 - gitlab-org/api/client-go + # (optional) Backup all public projects of these groups + # (including subgroups), even if you are not a member. + # Will be merged with the other discovery mechanisms + # and it's result filtered with excluded repos. + # (default: []) + organization: + - gnuwget # (optional) Exclude this list of repos # or whole organizations/users exclude: diff --git a/github.go b/github.go index 196bb19..4223e3e 100644 --- a/github.go +++ b/github.go @@ -23,6 +23,7 @@ type GithubConfig struct { Owned *bool `yaml:"owned,omitempty"` Exclude []string `yaml:"exclude,omitempty"` Repositories []string `yaml:"repositories,omitempty"` + Organization []string `yaml:"organization,omitempty"` client *github.Client } @@ -116,6 +117,21 @@ func (c *GithubConfig) getAllRepos() ([]*github.Repository, error) { // use discovery mechanisms var err error + for _, org := range c.Organization { + for repos, response, apiErr := c.getOrgRepos(org, 1); true; repos, response, apiErr = c.getOrgRepos(org, response.NextPage) { + if apiErr != nil { + return nil, apiErr + } + for _, repo := range repos { + all[*repo.FullName] = repo + } + + if len(repos) == 0 || response.NextPage == 0 { + break + } + } + } + for repos, response, apiErr := c.discoverRepos(1); true; repos, response, apiErr = c.discoverRepos(response.NextPage) { if apiErr != nil { err = apiErr @@ -175,6 +191,16 @@ func (c *GithubConfig) getRepoList(repos []string) ([]*github.Repository, error) return ghRepos, nil } +func (c *GithubConfig) getOrgRepos(org string, page int) ([]*github.Repository, *github.Response, error) { + return c.client.Repositories.ListByOrg(context.Background(), org, &github.RepositoryListByOrgOptions{ + Type: "public", + ListOptions: github.ListOptions{ + Page: page, + PerPage: 100, + }, + }) +} + func (c *GithubConfig) discoverRepos(page int) ([]*github.Repository, *github.Response, error) { affiliations := make([]string, 0) diff --git a/gitlab.go b/gitlab.go index f7ad988..891f127 100644 --- a/gitlab.go +++ b/gitlab.go @@ -18,6 +18,7 @@ type GitLabConfig struct { Owned *bool `yaml:"owned,omitempty"` Exclude []string `yaml:"exclude,omitempty"` Repositories []string `yaml:"repositories,omitempty"` + Organization []string `yaml:"organization,omitempty"` client *gitlab.Client } @@ -101,6 +102,16 @@ func (g *GitLabConfig) getAllRepos() ([]*gitlab.Project, error) { } // use discovery mechanisms + for _, group := range g.Organization { + if repos, err := g.getGroupRepos(group); err != nil { + return nil, err + } else { + for _, repo := range repos { + all[repo.PathWithNamespace] = repo + } + } + } + if *g.Starred { if repos, err := g.discoverRepos(&gitlab.ListProjectsOptions{Starred: boolPointer(true)}); err != nil { return nil, err @@ -149,6 +160,32 @@ func (g *GitLabConfig) getRepoList(repos []string) ([]*gitlab.Project, error) { } +func (g *GitLabConfig) getGroupRepos(group string) ([]*gitlab.Project, error) { + opts := &gitlab.ListGroupProjectsOptions{ + IncludeSubGroups: boolPointer(true), + Simple: boolPointer(true), + Visibility: gitlab.Ptr(gitlab.PublicVisibility), + } + opts.ListOptions.PerPage = 100 + + repos := make([]*gitlab.Project, 0, 100) + + // paginate + for i := 1; true; i++ { + opts.ListOptions.Page = i + projects, _, err := g.client.Groups.ListGroupProjects(group, opts) + if err != nil { + return nil, err + } + + if len(projects) == 0 { + break + } + repos = append(repos, projects...) + } + return repos, nil +} + func (g *GitLabConfig) discoverRepos(opts *gitlab.ListProjectsOptions) ([]*gitlab.Project, error) { opts.ListOptions.PerPage = 100