mirror of
https://github.com/gabrie30/ghorg.git
synced 2026-04-14 17:51:08 +02:00
Add GHORG_GITHUB_USER_GISTS flag to clone a GitHub user's gists (#626)
This commit is contained in:
parent
9321322a65
commit
e3a4dbcb49
79
cmd/clone.go
79
cmd/clone.go
@ -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())
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -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
|
||||
|
||||
116
scm/github.go
116
scm/github.go
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user