Add GHORG_GITHUB_USER_GISTS flag to clone a GitHub user's gists (#626)

This commit is contained in:
Copilot 2026-03-09 18:30:08 -07:00 committed by GitHub
parent 9321322a65
commit e3a4dbcb49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 481 additions and 9 deletions

View File

@ -303,6 +303,10 @@ func cloneFunc(cmd *cobra.Command, argz []string) {
os.Setenv("GHORG_CLONE_SNIPPETS", "true")
}
if cmd.Flags().Changed("github-user-gists") {
os.Setenv("GHORG_GITHUB_USER_GISTS", "true")
}
if cmd.Flags().Changed("insecure-gitlab-client") {
os.Setenv("GHORG_INSECURE_GITLAB_CLIENT", "true")
}
@ -434,6 +438,35 @@ func setupRepoClone() {
cachedDirSizeMB = 0
isDirSizeCached = false
if os.Getenv("GHORG_GITHUB_USER_GISTS") == "true" {
if os.Getenv("GHORG_SCM_TYPE") != "github" {
colorlog.PrintErrorAndExit("GHORG_GITHUB_USER_GISTS is only supported for GitHub, please set --scm=github")
}
if os.Getenv("GHORG_CLONE_TYPE") != "user" {
colorlog.PrintErrorAndExit("GHORG_GITHUB_USER_GISTS is only supported for user clones, please set --clone-type=user")
}
gistTargets, err := getAllUserGistCloneUrls()
if err != nil {
colorlog.PrintError("Encountered an error fetching gists, aborting")
fmt.Println(err)
os.Exit(1)
}
if len(gistTargets) == 0 {
colorlog.PrintInfo("No gists found for github user: " + targetCloneSource + ", please verify you have sufficient permissions, double check spelling and try again.")
os.Exit(0)
}
// Clone gists into a ghorg-gists subdirectory within the user's clone directory
originalOutputDirAbsolutePath := outputDirAbsolutePath
outputDirAbsolutePath = filepath.Join(originalOutputDirAbsolutePath, "ghorg-gists")
g := git.NewGit()
CloneAllRepos(g, gistTargets)
outputDirAbsolutePath = originalOutputDirAbsolutePath
return
}
var cloneTargets []scm.Repo
var err error
@ -456,8 +489,8 @@ func setupRepoClone() {
colorlog.PrintInfo("No repos found for " + os.Getenv("GHORG_SCM_TYPE") + " " + os.Getenv("GHORG_CLONE_TYPE") + ": " + targetCloneSource + ", please verify you have sufficient permissions to clone target repos, double check spelling and try again.")
os.Exit(0)
}
git := git.NewGit()
CloneAllRepos(git, cloneTargets)
g := git.NewGit()
CloneAllRepos(g, cloneTargets)
}
func getAllOrgCloneUrls() ([]scm.Repo, error) {
@ -468,6 +501,23 @@ func getAllUserCloneUrls() ([]scm.Repo, error) {
return getCloneUrls(false)
}
func getAllUserGistCloneUrls() ([]scm.Repo, error) {
asciiTime()
PrintConfigs()
client, err := scm.GetClient("github")
if err != nil {
colorlog.PrintError(err)
os.Exit(1)
}
githubClient, ok := client.(scm.Github)
if !ok {
colorlog.PrintErrorAndExit("Unable to cast client to GitHub client for gist fetching")
}
return githubClient.GetUserGists(targetCloneSource)
}
func getCloneUrls(isOrg bool) ([]scm.Repo, error) {
asciiTime()
PrintConfigs()
@ -658,19 +708,21 @@ func trimCollisionFilename(filename string) string {
return filename
}
func getCloneableInventory(allRepos []scm.Repo) (int, int, int, int) {
var wikis, snippets, repos, total int
func getCloneableInventory(allRepos []scm.Repo) (int, int, int, int, int) {
var wikis, snippets, repos, gists, total int
for _, repo := range allRepos {
if repo.IsGitLabSnippet {
snippets++
} else if repo.IsWiki {
wikis++
} else if repo.IsGitHubGist {
gists++
} else {
repos++
}
}
total = repos + snippets + wikis
return total, repos, snippets, wikis
total = repos + snippets + wikis + gists
return total, repos, snippets, wikis, gists
}
func isGitRepository(path string) bool {
@ -702,10 +754,12 @@ func CloneAllRepos(git git.Gitter, cloneTargets []scm.Repo) {
filter := NewRepositoryFilter()
cloneTargets = filter.ApplyAllFilters(cloneTargets)
totalResourcesToClone, reposToCloneCount, snippetToCloneCount, wikisToCloneCount := getCloneableInventory(cloneTargets)
totalResourcesToClone, reposToCloneCount, snippetToCloneCount, wikisToCloneCount, gistsToCloneCount := getCloneableInventory(cloneTargets)
if os.Getenv("GHORG_CLONE_WIKI") == "true" && os.Getenv("GHORG_CLONE_SNIPPETS") == "true" {
m := fmt.Sprintf("%v resources to clone found in %v, %v repos, %v snippets, and %v wikis\n", totalResourcesToClone, targetCloneSource, snippetToCloneCount, reposToCloneCount, wikisToCloneCount)
if os.Getenv("GHORG_GITHUB_USER_GISTS") == "true" {
colorlog.PrintInfo(fmt.Sprintf("%v gists found for %v\n", gistsToCloneCount, targetCloneSource))
} else if os.Getenv("GHORG_CLONE_WIKI") == "true" && os.Getenv("GHORG_CLONE_SNIPPETS") == "true" {
m := fmt.Sprintf("%v resources to clone found in %v, %v repos, %v snippets, and %v wikis\n", totalResourcesToClone, targetCloneSource, reposToCloneCount, snippetToCloneCount, wikisToCloneCount)
colorlog.PrintInfo(m)
} else if os.Getenv("GHORG_CLONE_WIKI") == "true" {
m := fmt.Sprintf("%v resources to clone found in %v, %v repos and %v wikis\n", totalResourcesToClone, targetCloneSource, reposToCloneCount, wikisToCloneCount)
@ -757,6 +811,10 @@ func CloneAllRepos(git git.Gitter, cloneTargets []scm.Repo) {
// The URL handling in getAppNameFromURL makes strong presumptions that the URL will end in an
// extension like '.git', but this is not the case for sourcehut (and possibly other forges).
repoSlug = repo.Name
} else if repo.IsGitHubGist {
// Gist folder names are pre-computed in filterGists from the primary filename
// (without extension, lowercased, with collision suffix appended when needed).
repoSlug = repo.Name
} else if repo.IsGitLabSnippet && !repo.IsGitLabRootLevelSnippet {
repoSlug = getAppNameFromURL(repo.GitLabSnippetInfo.URLOfRepo)
} else if repo.IsGitLabRootLevelSnippet {
@ -1277,6 +1335,9 @@ func PrintConfigs() {
if os.Getenv("GHORG_CLONE_SNIPPETS") == "true" {
colorlog.PrintInfo("* Snippets : " + os.Getenv("GHORG_CLONE_SNIPPETS"))
}
if os.Getenv("GHORG_GITHUB_USER_GISTS") == "true" {
colorlog.PrintInfo("* Gists : " + os.Getenv("GHORG_GITHUB_USER_GISTS"))
}
if configs.GhorgIgnoreDetected() {
colorlog.PrintInfo("* Ghorgignore : " + configs.GhorgIgnoreLocation())
}

View File

@ -1113,3 +1113,78 @@ func TestPrintFinishedWithDirSize_NoTiming(t *testing.T) {
// The function should complete without error and not include timing info
}
func TestGetCloneableInventoryWithGists(t *testing.T) {
repos := []scm.Repo{
{Name: "repo1"},
{Name: "repo2"},
{Name: "wiki1", IsWiki: true},
{Name: "gist1", IsGitHubGist: true},
{Name: "gist2", IsGitHubGist: true},
}
total, repoCount, snippetCount, wikiCount, gistCount := getCloneableInventory(repos)
if total != 5 {
t.Errorf("Expected total 5, got %d", total)
}
if repoCount != 2 {
t.Errorf("Expected repoCount 2, got %d", repoCount)
}
if snippetCount != 0 {
t.Errorf("Expected snippetCount 0, got %d", snippetCount)
}
if wikiCount != 1 {
t.Errorf("Expected wikiCount 1, got %d", wikiCount)
}
if gistCount != 2 {
t.Errorf("Expected gistCount 2, got %d", gistCount)
}
}
func TestGithubUserGistsValidation(t *testing.T) {
t.Run("GHORG_GITHUB_USER_GISTS requires github scm type", func(tt *testing.T) {
defer UnsetEnv("GHORG_")()
os.Setenv("GHORG_GITHUB_USER_GISTS", "true")
os.Setenv("GHORG_SCM_TYPE", "gitlab")
os.Setenv("GHORG_CLONE_TYPE", "user")
// Verify the condition that setupRepoClone checks
if os.Getenv("GHORG_GITHUB_USER_GISTS") == "true" && os.Getenv("GHORG_SCM_TYPE") != "github" {
// This is the expected invalid state - gists + non-github scm should error
// The setupRepoClone function calls colorlog.PrintErrorAndExit in this case
} else {
tt.Errorf("Expected GHORG_GITHUB_USER_GISTS=true with non-github SCM to be an invalid combination")
}
})
t.Run("GHORG_GITHUB_USER_GISTS requires user clone type", func(tt *testing.T) {
defer UnsetEnv("GHORG_")()
os.Setenv("GHORG_GITHUB_USER_GISTS", "true")
os.Setenv("GHORG_SCM_TYPE", "github")
os.Setenv("GHORG_CLONE_TYPE", "org")
// Verify the condition that setupRepoClone checks
if os.Getenv("GHORG_GITHUB_USER_GISTS") == "true" && os.Getenv("GHORG_CLONE_TYPE") != "user" {
// This is the expected invalid state - gists + org clone type should error
// The setupRepoClone function calls colorlog.PrintErrorAndExit in this case
} else {
tt.Errorf("Expected GHORG_GITHUB_USER_GISTS=true with org clone type to be an invalid combination")
}
})
t.Run("GHORG_GITHUB_USER_GISTS with valid github user combination", func(tt *testing.T) {
defer UnsetEnv("GHORG_")()
os.Setenv("GHORG_GITHUB_USER_GISTS", "true")
os.Setenv("GHORG_SCM_TYPE", "github")
os.Setenv("GHORG_CLONE_TYPE", "user")
// Verify the valid combination passes the check
if os.Getenv("GHORG_GITHUB_USER_GISTS") != "true" || os.Getenv("GHORG_SCM_TYPE") != "github" || os.Getenv("GHORG_CLONE_TYPE") != "user" {
tt.Errorf("Expected valid github user gists combination to be accepted")
}
})
}

View File

@ -75,6 +75,7 @@ var (
ghorgReCloneList bool
ghorgReCloneEnvConfigOnly bool
githubTokenFromGithubApp bool
githubUserGists bool
noToken bool
quietMode bool
noDirSize bool
@ -232,6 +233,8 @@ func getOrSetDefaults(envVar string) {
os.Setenv(envVar, "1")
case "GHORG_GITHUB_TOKEN_FROM_GITHUB_APP":
os.Setenv(envVar, "false")
case "GHORG_GITHUB_USER_GISTS":
os.Setenv(envVar, "false")
case "GHORG_SSH_HOSTNAME":
os.Setenv(envVar, "")
}
@ -330,6 +333,7 @@ func InitConfig() {
getOrSetDefaults("GHORG_CLONE_DEPTH")
getOrSetDefaults("GHORG_GITHUB_TOKEN")
getOrSetDefaults("GHORG_GITHUB_TOKEN_FROM_GITHUB_APP")
getOrSetDefaults("GHORG_GITHUB_USER_GISTS")
getOrSetDefaults("GHORG_GITHUB_FILTER_LANGUAGE")
getOrSetDefaults("GHORG_COLOR")
getOrSetDefaults("GHORG_TOPICS")
@ -429,6 +433,7 @@ func init() {
cloneCmd.Flags().StringVarP(&exitCodeOnCloneIssues, "exit-code-on-clone-issues", "", "", "GHORG_EXIT_CODE_ON_CLONE_ISSUES - Exit code when issues/errors occur during cloning. Useful for CI/CD failure detection (default: 1)")
cloneCmd.Flags().StringVarP(&gitFilter, "git-filter", "", "", "GHORG_GIT_FILTER - Arguments to pass to git's --filter flag. Use --git-filter=blob:none to exclude binary objects and reduce clone size. Requires git 2.19+")
cloneCmd.Flags().BoolVarP(&githubTokenFromGithubApp, "github-token-from-github-app", "", false, "GHORG_GITHUB_TOKEN_FROM_GITHUB_APP - GitHub only: Treat the provided token as a GitHub App token (when obtained outside ghorg). Use with pre-generated app tokens")
cloneCmd.Flags().BoolVarP(&githubUserGists, "github-user-gists", "", false, "GHORG_GITHUB_USER_GISTS - GitHub only: Clone all of a user's gists into clone-dir/ghorg-gists. Requires --clone-type=user and --scm=github")
cloneCmd.Flags().StringVarP(&githubAppPemPath, "github-app-pem-path", "", "", "GHORG_GITHUB_APP_PEM_PATH - GitHub only: Path to GitHub App private key (.pem file) for app-based authentication. Requires --github-app-id and --github-app-installation-id")
cloneCmd.Flags().StringVarP(&githubAppInstallationID, "github-app-installation-id", "", "", "GHORG_GITHUB_APP_INSTALLATION_ID - GitHub only: Installation ID for GitHub App authentication. Find in org settings URL")
cloneCmd.Flags().StringVarP(&githubFilterLanguage, "github-filter-language", "", "", "GHORG_GITHUB_FILTER_LANGUAGE - GitHub only: Filter repositories by programming language. Comma-separated values (e.g., --github-filter-language=go,python)")

View File

@ -240,6 +240,11 @@ GHORG_GITHUB_APP_ID:
# Can be one of: all, owner, member (default: owner)
GHORG_GITHUB_USER_OPTION:
# Clone all of a user's gists into clone-dir/ghorg-gists
# Only available when using GHORG_SCM_TYPE: github and GHORG_CLONE_TYPE: user
# flag (--github-user-gists)
GHORG_GITHUB_USER_GISTS: false
# Filter repos by a language
# Can be a comma separated value with no spaces
# falg (--github-filter-language) e.g.: --github-filter-language=go,ruby,elixir

View File

@ -6,6 +6,8 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
@ -266,6 +268,120 @@ func (c Github) filter(allRepos []*github.Repository) []Repo {
return repoData
}
// GetUserGists gets all gists for a GitHub user
func (c Github) GetUserGists(targetUser string) ([]Repo, error) {
c.SetTokensUsername()
spinningSpinner.Start()
defer spinningSpinner.Stop()
opt := &github.GistListOptions{
ListOptions: github.ListOptions{PerPage: reposPerPage, Page: 1},
}
gists, resp, err := c.Gists.List(context.Background(), targetUser, opt)
if err != nil {
return nil, err
}
allGists := gists
for page := 2; page <= resp.LastPage; page++ {
opt.Page = page
g, _, err := c.Gists.List(context.Background(), targetUser, opt)
if err != nil {
return nil, err
}
allGists = append(allGists, g...)
}
return c.filterGists(allGists), nil
}
// gistFolderName returns the folder name to use for a gist, derived from the
// gist's primary filename (first filename alphabetically). The extension is
// stripped and the result is lowercased. Falls back to the gist ID when the
// gist has no files or the derived base name is empty.
func gistFolderName(gist *github.Gist) string {
if len(gist.Files) == 0 {
return strings.ToLower(*gist.ID)
}
// Collect and sort filenames for a stable, deterministic result.
filenames := make([]string, 0, len(gist.Files))
for fn := range gist.Files {
filenames = append(filenames, string(fn))
}
sort.Strings(filenames)
firstName := filenames[0]
ext := filepath.Ext(firstName)
base := strings.ToLower(firstName[:len(firstName)-len(ext)])
if base == "" {
return strings.ToLower(*gist.ID)
}
return base
}
func (c Github) filterGists(allGists []*github.Gist) []Repo {
// First pass: compute a derived folder name for every valid gist and
// count how many gists share the same name so we can detect collisions.
type gistEntry struct {
gist *github.Gist
folderName string
}
entries := make([]gistEntry, 0, len(allGists))
nameCount := make(map[string]int)
for _, gist := range allGists {
if gist.ID == nil || gist.GitPullURL == nil {
continue
}
name := gistFolderName(gist)
entries = append(entries, gistEntry{gist: gist, folderName: name})
nameCount[name]++
}
// Second pass: build Repo records, appending the gist ID to the folder
// name whenever two or more gists share the same derived name.
var repoData []Repo
for _, entry := range entries {
gist := entry.gist
folderName := entry.folderName
if nameCount[folderName] > 1 {
folderName = folderName + "-" + *gist.ID
}
r := Repo{}
r.ID = *gist.ID
r.Name = folderName
r.Path = folderName
r.IsGitHubGist = true
if os.Getenv("GHORG_BRANCH") != "" {
r.CloneBranch = os.Getenv("GHORG_BRANCH")
} else {
r.CloneBranch = "master"
}
r.URL = *gist.GitPullURL
if os.Getenv("GHORG_CLONE_PROTOCOL") == "https" || os.Getenv("GHORG_GITHUB_APP_PEM_PATH") != "" {
r.CloneURL = c.addTokenToHTTPSCloneURL(*gist.GitPullURL, os.Getenv("GHORG_GITHUB_TOKEN"))
} else {
// Convert HTTPS gist pull URL to SSH URL
// https://gist.github.com/<id>.git → git@gist.github.com:<id>.git
sshURL := strings.Replace(*gist.GitPullURL, "https://gist.github.com/", "git@gist.github.com:", 1)
r.CloneURL = ReplaceSSHHostname(sshURL, os.Getenv("GHORG_SSH_HOSTNAME"))
}
repoData = append(repoData, r)
}
return repoData
}
// Sets the GitHub username tied to the github token to the package variable tokenUsername
// Then if https clone method is used the clone url will be https://username:token@github.com/org/repo.git
// The username is now needed when using the new fine-grained tokens for github

View File

@ -134,3 +134,211 @@ func TestGetOrgRepos(t *testing.T) {
os.Setenv("GHORG_TOPICS", "")
})
}
func TestGetUserGists(t *testing.T) {
client, mux, _, teardown := setup()
github := Github{Client: client}
defer teardown()
mux.HandleFunc("/users/testuser/gists", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `[
{"id":"abc123", "git_pull_url": "https://gist.github.com/abc123.git", "public": true, "files": {"foobar.md": {"filename": "foobar.md"}}},
{"id":"def456", "git_pull_url": "https://gist.github.com/def456.git", "public": true, "files": {"script.sh": {"filename": "script.sh"}}},
{"id":"ghi789", "git_pull_url": "https://gist.github.com/ghi789.git", "public": false, "files": {"README.txt": {"filename": "README.txt"}}}
]`)
})
t.Run("Should return all gists with HTTPS protocol", func(tt *testing.T) {
os.Setenv("GHORG_CLONE_PROTOCOL", "https")
os.Setenv("GHORG_GITHUB_TOKEN", "testtoken")
resp, err := github.GetUserGists("testuser")
if err != nil {
tt.Fatal(err)
}
want := 3
got := len(resp)
if want != got {
tt.Errorf("Expected %v gists, got: %v", want, got)
}
for _, repo := range resp {
if !repo.IsGitHubGist {
tt.Errorf("Expected IsGitHubGist to be true for gist %s", repo.Name)
}
}
os.Unsetenv("GHORG_CLONE_PROTOCOL")
os.Unsetenv("GHORG_GITHUB_TOKEN")
})
t.Run("Should use filename without extension as folder name", func(tt *testing.T) {
os.Setenv("GHORG_CLONE_PROTOCOL", "https")
os.Setenv("GHORG_GITHUB_TOKEN", "testtoken")
resp, err := github.GetUserGists("testuser")
if err != nil {
tt.Fatal(err)
}
// Build a map of ID→Name for easy lookup
names := make(map[string]string)
for _, repo := range resp {
names[repo.ID] = repo.Name
}
tests := []struct{ id, wantName string }{
{"abc123", "foobar"}, // foobar.md → foobar
{"def456", "script"}, // script.sh → script
{"ghi789", "readme"}, // README.txt → readme (lowercased)
}
for _, tc := range tests {
if got := names[tc.id]; got != tc.wantName {
tt.Errorf("gist %s: expected folder name %q, got %q", tc.id, tc.wantName, got)
}
}
os.Unsetenv("GHORG_CLONE_PROTOCOL")
os.Unsetenv("GHORG_GITHUB_TOKEN")
})
t.Run("Should set correct clone branch for gists", func(tt *testing.T) {
os.Setenv("GHORG_CLONE_PROTOCOL", "https")
os.Setenv("GHORG_GITHUB_TOKEN", "testtoken")
resp, err := github.GetUserGists("testuser")
if err != nil {
tt.Fatal(err)
}
// When GHORG_BRANCH is not set, gists default to "master"
for _, repo := range resp {
if repo.CloneBranch != "master" {
tt.Errorf("Expected CloneBranch to be 'master' when GHORG_BRANCH is unset, got: %s", repo.CloneBranch)
}
}
os.Unsetenv("GHORG_CLONE_PROTOCOL")
os.Unsetenv("GHORG_GITHUB_TOKEN")
})
t.Run("Should respect GHORG_BRANCH for gists", func(tt *testing.T) {
os.Setenv("GHORG_CLONE_PROTOCOL", "https")
os.Setenv("GHORG_GITHUB_TOKEN", "testtoken")
os.Setenv("GHORG_BRANCH", "main")
resp, err := github.GetUserGists("testuser")
if err != nil {
tt.Fatal(err)
}
for _, repo := range resp {
if repo.CloneBranch != "main" {
tt.Errorf("Expected CloneBranch to be 'main', got: %s", repo.CloneBranch)
}
}
os.Unsetenv("GHORG_CLONE_PROTOCOL")
os.Unsetenv("GHORG_GITHUB_TOKEN")
os.Unsetenv("GHORG_BRANCH")
})
}
func TestGistFolderName(t *testing.T) {
strPtr := func(s string) *string { return &s }
tests := []struct {
name string
gist *ghpkg.Gist
wantName string
}{
{
name: "single file with extension",
gist: &ghpkg.Gist{ID: strPtr("id1"), Files: map[ghpkg.GistFilename]ghpkg.GistFile{"foobar.md": {}}},
wantName: "foobar",
},
{
name: "uppercase extension is lowercased",
gist: &ghpkg.Gist{ID: strPtr("id2"), Files: map[ghpkg.GistFilename]ghpkg.GistFile{"README.txt": {}}},
wantName: "readme",
},
{
name: "no extension",
gist: &ghpkg.Gist{ID: strPtr("id3"), Files: map[ghpkg.GistFilename]ghpkg.GistFile{"Makefile": {}}},
wantName: "makefile",
},
{
name: "multiple files uses first alphabetically",
gist: &ghpkg.Gist{ID: strPtr("id4"), Files: map[ghpkg.GistFilename]ghpkg.GistFile{"zebra.go": {}, "alpha.go": {}, "middle.go": {}}},
wantName: "alpha",
},
{
name: "no files falls back to gist id",
gist: &ghpkg.Gist{ID: strPtr("abc999"), Files: map[ghpkg.GistFilename]ghpkg.GistFile{}},
wantName: "abc999",
},
}
for _, tc := range tests {
t.Run(tc.name, func(tt *testing.T) {
got := gistFolderName(tc.gist)
if got != tc.wantName {
tt.Errorf("gistFolderName: expected %q, got %q", tc.wantName, got)
}
})
}
}
func TestFilterGistsCollisions(t *testing.T) {
client, mux, _, teardown := setup()
gh := Github{Client: client}
defer teardown()
// Two gists share the same derived folder name ("foobar")
mux.HandleFunc("/users/collisionuser/gists", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `[
{"id":"aaa111", "git_pull_url": "https://gist.github.com/aaa111.git", "files": {"foobar.md": {"filename": "foobar.md"}}},
{"id":"bbb222", "git_pull_url": "https://gist.github.com/bbb222.git", "files": {"foobar.sh": {"filename": "foobar.sh"}}},
{"id":"ccc333", "git_pull_url": "https://gist.github.com/ccc333.git", "files": {"unique.py": {"filename": "unique.py"}}}
]`)
})
os.Setenv("GHORG_CLONE_PROTOCOL", "https")
os.Setenv("GHORG_GITHUB_TOKEN", "testtoken")
defer func() {
os.Unsetenv("GHORG_CLONE_PROTOCOL")
os.Unsetenv("GHORG_GITHUB_TOKEN")
}()
resp, err := gh.GetUserGists("collisionuser")
if err != nil {
t.Fatal(err)
}
if len(resp) != 3 {
t.Fatalf("Expected 3 repos, got %d", len(resp))
}
names := make(map[string]string) // id → Name
for _, repo := range resp {
names[repo.ID] = repo.Name
}
// Both "foobar.md" and "foobar.sh" collide on "foobar", so both get the gist ID appended
if got := names["aaa111"]; got != "foobar-aaa111" {
t.Errorf("aaa111: expected 'foobar-aaa111', got %q", got)
}
if got := names["bbb222"]; got != "foobar-bbb222" {
t.Errorf("bbb222: expected 'foobar-bbb222', got %q", got)
}
// No collision for "unique"
if got := names["ccc333"]; got != "unique" {
t.Errorf("ccc333: expected 'unique', got %q", got)
}
}

View File

@ -22,6 +22,8 @@ type Repo struct {
IsGitLabSnippet bool
// IsGitLabRootLevelSnippet is set to true when a snippet was not created for a repo
IsGitLabRootLevelSnippet bool
// IsGitHubGist is set to true when the data is for a github gist
IsGitHubGist bool
// GitLabSnippetInfo provides additional information when the thing we are cloning is a gitlab snippet
GitLabSnippetInfo GitLabSnippet
Commits RepoCommits