Empty repos should not fail on pull (#513)

This commit is contained in:
Elmar Fasel 2025-04-30 03:34:07 +02:00 committed by GitHub
parent 4500043663
commit a1395a2a6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 133 additions and 17 deletions

View File

@ -852,9 +852,23 @@ func CloneAllRepos(git git.Gitter, cloneTargets []scm.Repo) {
// Retry checkout // Retry checkout
errRetry := git.Checkout(repo) errRetry := git.Checkout(repo)
if errRetry != nil { if errRetry != nil {
e := fmt.Sprintf("Could not checkout out %s, branch may not exist or may not have any contents/commits, no changes made on: %s Error: %v", repo.CloneBranch, repo.URL, errRetry) hasRemoteHeads, errHasRemoteHeads := git.HasRemoteHeads(repo)
cloneErrors = append(cloneErrors, e) if errHasRemoteHeads != nil {
return e := fmt.Sprintf("Could not checkout %s, branch may not exist or may not have any contents/commits, no changes made on: %s Errors: %v %v", repo.CloneBranch, repo.URL, errRetry, errHasRemoteHeads)
cloneErrors = append(cloneErrors, e)
return
}
if hasRemoteHeads {
// weird, should not happen, return original checkout error
e := fmt.Sprintf("Could not checkout %s, branch may not exist or may not have any contents/commits, no changes made on: %s Error: %v", repo.CloneBranch, repo.URL, errRetry)
cloneErrors = append(cloneErrors, e)
return
} else {
// this is _just_ an empty repository
e := fmt.Sprintf("Could not checkout %s due to repository being empty, no changes made on: %s", repo.CloneBranch, repo.URL)
cloneInfos = append(cloneInfos, e)
return
}
} }
} }
@ -1032,24 +1046,29 @@ func CloneAllRepos(git git.Gitter, cloneTargets []scm.Repo) {
writeGhorgStats(date, allReposToCloneCount, cloneCount, pulledCount, cloneInfosCount, cloneErrorsCount, updateRemoteCount, newCommits, pruneCount, hasCollisions) writeGhorgStats(date, allReposToCloneCount, cloneCount, pulledCount, cloneInfosCount, cloneErrorsCount, updateRemoteCount, newCommits, pruneCount, hasCollisions)
} }
if os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS") != "0" && cloneInfosCount > 0 { if os.Getenv("GHORG_DONT_EXIT_UNDER_TEST") != "true" {
exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS")) if os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS") != "0" && cloneInfosCount > 0 {
if err != nil { exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS"))
colorlog.PrintError("Could not convert GHORG_EXIT_CODE_ON_CLONE_INFOS from string to integer") if err != nil {
os.Exit(1) colorlog.PrintError("Could not convert GHORG_EXIT_CODE_ON_CLONE_INFOS from string to integer")
} os.Exit(1)
}
os.Exit(exitCode) os.Exit(exitCode)
}
} }
if cloneErrorsCount > 0 { if os.Getenv("GHORG_DONT_EXIT_UNDER_TEST") != "true" {
exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_ISSUES")) if cloneErrorsCount > 0 {
if err != nil { exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_ISSUES"))
colorlog.PrintError("Could not convert GHORG_EXIT_CODE_ON_CLONE_ISSUES from string to integer") if err != nil {
os.Exit(1) colorlog.PrintError("Could not convert GHORG_EXIT_CODE_ON_CLONE_ISSUES from string to integer")
os.Exit(1)
}
os.Exit(exitCode)
} }
} else {
os.Exit(exitCode) cloneErrorsCount = 0
} }
} }

View File

@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"errors"
"log" "log"
"os" "os"
"reflect" "reflect"
@ -13,6 +14,7 @@ import (
func TestShouldLowerRegularString(t *testing.T) { func TestShouldLowerRegularString(t *testing.T) {
upperName := "RepoName" upperName := "RepoName"
defer setOutputDirName([]string{""})
setOutputDirName([]string{upperName}) setOutputDirName([]string{upperName})
if outputDirName != "reponame" { if outputDirName != "reponame" {
@ -23,6 +25,7 @@ func TestShouldLowerRegularString(t *testing.T) {
func TestShouldNotChangeLowerCasedRegularString(t *testing.T) { func TestShouldNotChangeLowerCasedRegularString(t *testing.T) {
lowerName := "repo_name" lowerName := "repo_name"
defer setOutputDirName([]string{""})
setOutputDirName([]string{lowerName}) setOutputDirName([]string{lowerName})
if outputDirName != "repo_name" { if outputDirName != "repo_name" {
@ -34,6 +37,7 @@ func TestReplaceDashWithUnderscore(t *testing.T) {
want := "repo-name" want := "repo-name"
lowerName := "repo-name" lowerName := "repo-name"
defer setOutputDirName([]string{""})
setOutputDirName([]string{lowerName}) setOutputDirName([]string{lowerName})
if outputDirName != want { if outputDirName != want {
@ -44,6 +48,7 @@ func TestReplaceDashWithUnderscore(t *testing.T) {
func TestShouldNotChangeNonLettersString(t *testing.T) { func TestShouldNotChangeNonLettersString(t *testing.T) {
numberName := "1234567_8" numberName := "1234567_8"
defer setOutputDirName([]string{""})
setOutputDirName([]string{numberName}) setOutputDirName([]string{numberName})
if outputDirName != "1234567_8" { if outputDirName != "1234567_8" {
@ -57,6 +62,13 @@ func NewMockGit() MockGitClient {
return MockGitClient{} return MockGitClient{}
} }
func (g MockGitClient) HasRemoteHeads(repo scm.Repo) (bool, error) {
if repo.Name == "testRepoEmpty" {
return false, nil
}
return true, nil
}
func (g MockGitClient) Clone(repo scm.Repo) error { func (g MockGitClient) Clone(repo scm.Repo) error {
_, err := os.MkdirTemp(os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"), repo.Name) _, err := os.MkdirTemp(os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"), repo.Name)
if err != nil { if err != nil {
@ -74,6 +86,9 @@ func (g MockGitClient) SetOriginWithCredentials(repo scm.Repo) error {
} }
func (g MockGitClient) Checkout(repo scm.Repo) error { func (g MockGitClient) Checkout(repo scm.Repo) error {
if repo.Name == "testRepoEmpty" {
return errors.New("Cannot checkout any specific branch in an empty repository")
}
return nil return nil
} }
@ -144,6 +159,49 @@ func TestInitialClone(t *testing.T) {
} }
} }
func TestCloneEmptyRepo(t *testing.T) {
defer UnsetEnv("GHORG_")()
dir, err := os.MkdirTemp("", "ghorg_test_empty_repo")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(dir)
os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir)
setOuputDirAbsolutePath()
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
// simulate a previous clone of empty git repository
repoErr := os.Mkdir(outputDirAbsolutePath+"/"+"testRepoEmpty", 0o700)
if repoErr != nil {
log.Fatal(repoErr)
}
defer os.RemoveAll(outputDirAbsolutePath + "/" + "testRepoEmpty")
os.Setenv("GHORG_CONCURRENCY", "1")
var testRepos = []scm.Repo{
{
Name: "testRepoEmpty",
URL: "git@github.com:org/testRepoEmpty.git",
CloneBranch: "main",
},
}
mockGit := NewMockGit()
CloneAllRepos(mockGit, testRepos)
gotInfos := len(cloneInfos)
expectedInfos := 1
if gotInfos != expectedInfos {
t.Fatalf("Wrong number of cloneInfos, expected: %v, got: %v", expectedInfos, gotInfos)
}
gotInfo := cloneInfos[0]
expected := "Could not checkout main due to repository being empty, no changes made on: git@github.com:org/testRepoEmpty.git"
if gotInfo != expected {
t.Errorf("Wrong cloneInfo, expected: %v, got: %v", expected, gotInfo)
}
}
func TestMatchPrefix(t *testing.T) { func TestMatchPrefix(t *testing.T) {
defer UnsetEnv("GHORG_")() defer UnsetEnv("GHORG_")()
dir, err := os.MkdirTemp("", "ghorg_test_match_prefix") dir, err := os.MkdirTemp("", "ghorg_test_match_prefix")
@ -154,6 +212,8 @@ func TestMatchPrefix(t *testing.T) {
os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir) os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir)
os.Setenv("GHORG_CONCURRENCY", "1") os.Setenv("GHORG_CONCURRENCY", "1")
os.Setenv("GHORG_MATCH_PREFIX", "test") os.Setenv("GHORG_MATCH_PREFIX", "test")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{ var testRepos = []scm.Repo{
{ {
Name: "testRepoOne", Name: "testRepoOne",
@ -191,6 +251,8 @@ func TestExcludeMatchPrefix(t *testing.T) {
os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir) os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir)
os.Setenv("GHORG_CONCURRENCY", "1") os.Setenv("GHORG_CONCURRENCY", "1")
os.Setenv("GHORG_EXCLUDE_MATCH_PREFIX", "test") os.Setenv("GHORG_EXCLUDE_MATCH_PREFIX", "test")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{ var testRepos = []scm.Repo{
{ {
Name: "testRepoOne", Name: "testRepoOne",
@ -228,6 +290,8 @@ func TestMatchRegex(t *testing.T) {
os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir) os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir)
os.Setenv("GHORG_CONCURRENCY", "1") os.Setenv("GHORG_CONCURRENCY", "1")
os.Setenv("GHORG_MATCH_REGEX", "^test-") os.Setenv("GHORG_MATCH_REGEX", "^test-")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{ var testRepos = []scm.Repo{
{ {
Name: "test-RepoOne", Name: "test-RepoOne",
@ -267,6 +331,8 @@ func TestExcludeMatchRegex(t *testing.T) {
os.Setenv("GHORG_CONCURRENCY", "1") os.Setenv("GHORG_CONCURRENCY", "1")
os.Setenv("GHORG_OUTPUT_DIR", testDescriptor) os.Setenv("GHORG_OUTPUT_DIR", testDescriptor)
os.Setenv("GHORG_EXCLUDE_MATCH_REGEX", "^test-") os.Setenv("GHORG_EXCLUDE_MATCH_REGEX", "^test-")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{ var testRepos = []scm.Repo{
{ {
Name: "test-RepoOne", Name: "test-RepoOne",

View File

@ -1,6 +1,7 @@
package git package git
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -26,6 +27,7 @@ type Gitter interface {
FetchAll(scm.Repo) error FetchAll(scm.Repo) error
FetchCloneBranch(scm.Repo) error FetchCloneBranch(scm.Repo) error
RepoCommitCount(scm.Repo) (int, error) RepoCommitCount(scm.Repo) (int, error)
HasRemoteHeads(scm.Repo) (bool, error)
} }
type GitClient struct{} type GitClient struct{}
@ -51,6 +53,35 @@ func printDebugCmd(cmd *exec.Cmd, repo scm.Repo) error {
return err return err
} }
func (g GitClient) HasRemoteHeads(repo scm.Repo) (bool, error) {
cmd := exec.Command("git", "ls-remote", "--heads", "--quiet", "--exit-code")
err := cmd.Run()
if err == nil {
// successfully listed the remote heads
return true, nil
}
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
// error, but no exit code, return err
return false, err
}
exitCode := exitError.ExitCode()
if exitCode == 0 {
// ls-remote did successfully list the remote heads
return true, nil
} else if exitCode == 2 {
// repository is empty
return false, nil
} else {
// another exit code, simply return err
return false, err
}
}
func (g GitClient) Clone(repo scm.Repo) error { func (g GitClient) Clone(repo scm.Repo) error {
args := []string{"clone", repo.CloneURL, repo.HostPath} args := []string{"clone", repo.CloneURL, repo.HostPath}