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
errRetry := git.Checkout(repo)
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)
cloneErrors = append(cloneErrors, e)
return
hasRemoteHeads, errHasRemoteHeads := git.HasRemoteHeads(repo)
if errHasRemoteHeads != nil {
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)
}
if os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS") != "0" && cloneInfosCount > 0 {
exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS"))
if err != nil {
colorlog.PrintError("Could not convert GHORG_EXIT_CODE_ON_CLONE_INFOS from string to integer")
os.Exit(1)
}
if os.Getenv("GHORG_DONT_EXIT_UNDER_TEST") != "true" {
if os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS") != "0" && cloneInfosCount > 0 {
exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_INFOS"))
if err != nil {
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 {
exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_ISSUES"))
if err != nil {
colorlog.PrintError("Could not convert GHORG_EXIT_CODE_ON_CLONE_ISSUES from string to integer")
os.Exit(1)
if os.Getenv("GHORG_DONT_EXIT_UNDER_TEST") != "true" {
if cloneErrorsCount > 0 {
exitCode, err := strconv.Atoi(os.Getenv("GHORG_EXIT_CODE_ON_CLONE_ISSUES"))
if err != nil {
colorlog.PrintError("Could not convert GHORG_EXIT_CODE_ON_CLONE_ISSUES from string to integer")
os.Exit(1)
}
os.Exit(exitCode)
}
os.Exit(exitCode)
} else {
cloneErrorsCount = 0
}
}

View File

@ -1,6 +1,7 @@
package cmd
import (
"errors"
"log"
"os"
"reflect"
@ -13,6 +14,7 @@ import (
func TestShouldLowerRegularString(t *testing.T) {
upperName := "RepoName"
defer setOutputDirName([]string{""})
setOutputDirName([]string{upperName})
if outputDirName != "reponame" {
@ -23,6 +25,7 @@ func TestShouldLowerRegularString(t *testing.T) {
func TestShouldNotChangeLowerCasedRegularString(t *testing.T) {
lowerName := "repo_name"
defer setOutputDirName([]string{""})
setOutputDirName([]string{lowerName})
if outputDirName != "repo_name" {
@ -34,6 +37,7 @@ func TestReplaceDashWithUnderscore(t *testing.T) {
want := "repo-name"
lowerName := "repo-name"
defer setOutputDirName([]string{""})
setOutputDirName([]string{lowerName})
if outputDirName != want {
@ -44,6 +48,7 @@ func TestReplaceDashWithUnderscore(t *testing.T) {
func TestShouldNotChangeNonLettersString(t *testing.T) {
numberName := "1234567_8"
defer setOutputDirName([]string{""})
setOutputDirName([]string{numberName})
if outputDirName != "1234567_8" {
@ -57,6 +62,13 @@ func NewMockGit() 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 {
_, err := os.MkdirTemp(os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"), repo.Name)
if err != nil {
@ -74,6 +86,9 @@ func (g MockGitClient) SetOriginWithCredentials(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
}
@ -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) {
defer UnsetEnv("GHORG_")()
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_CONCURRENCY", "1")
os.Setenv("GHORG_MATCH_PREFIX", "test")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{
{
Name: "testRepoOne",
@ -191,6 +251,8 @@ func TestExcludeMatchPrefix(t *testing.T) {
os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir)
os.Setenv("GHORG_CONCURRENCY", "1")
os.Setenv("GHORG_EXCLUDE_MATCH_PREFIX", "test")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{
{
Name: "testRepoOne",
@ -228,6 +290,8 @@ func TestMatchRegex(t *testing.T) {
os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", dir)
os.Setenv("GHORG_CONCURRENCY", "1")
os.Setenv("GHORG_MATCH_REGEX", "^test-")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{
{
Name: "test-RepoOne",
@ -267,6 +331,8 @@ func TestExcludeMatchRegex(t *testing.T) {
os.Setenv("GHORG_CONCURRENCY", "1")
os.Setenv("GHORG_OUTPUT_DIR", testDescriptor)
os.Setenv("GHORG_EXCLUDE_MATCH_REGEX", "^test-")
os.Setenv("GHORG_DONT_EXIT_UNDER_TEST", "true")
var testRepos = []scm.Repo{
{
Name: "test-RepoOne",

View File

@ -1,6 +1,7 @@
package git
import (
"errors"
"fmt"
"os"
"os/exec"
@ -26,6 +27,7 @@ type Gitter interface {
FetchAll(scm.Repo) error
FetchCloneBranch(scm.Repo) error
RepoCommitCount(scm.Repo) (int, error)
HasRemoteHeads(scm.Repo) (bool, error)
}
type GitClient struct{}
@ -51,6 +53,35 @@ func printDebugCmd(cmd *exec.Cmd, repo scm.Repo) error {
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 {
args := []string{"clone", repo.CloneURL, repo.HostPath}