Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 35 additions & 20 deletions cmd/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"bufio"
"crypto/sha256"
"fmt"
"io/fs"
"log"
"net/url"
"os"
Expand Down Expand Up @@ -556,18 +557,18 @@ func printDryRun(repos []scm.Repo) {
// to do.
colorlog.PrintInfo("\nScanning for local clones that have been removed on remote...")

files, err := os.ReadDir(outputDirAbsolutePath)
repositories, err := getRelativePathRepositories(outputDirAbsolutePath)
if err != nil {
log.Fatal(err)
}

eligibleForPrune := 0
for _, f := range files {
for _, repository := range repositories {
// for each item in the org's clone directory, let's make sure we found a
// corresponding repo on the remote.
if !sliceContainsNamedRepo(repos, f.Name()) {
if !sliceContainsNamedRepo(repos, repository) {
eligibleForPrune++
colorlog.PrintSubtleInfo(fmt.Sprintf("%s not found in remote.", f.Name()))
colorlog.PrintSubtleInfo(fmt.Sprintf("%s not found in remote.", repository))
}
}
colorlog.PrintSuccess(fmt.Sprintf("Local clones eligible for pruning: %d", eligibleForPrune))
Expand Down Expand Up @@ -599,6 +600,29 @@ func getCloneableInventory(allRepos []scm.Repo) (int, int, int, int) {
return total, repos, snippets, wikis
}

func isGitRepository(path string) bool {
stat, err := os.Stat(filepath.Join(path, ".git"))
return err == nil && stat.IsDir()
}

func getRelativePathRepositories(root string) ([]string, error) {
var relativePaths []string
err := filepath.WalkDir(root, func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if path != outputDirAbsolutePath && file.IsDir() && isGitRepository(path) {
rel, err := filepath.Rel(outputDirAbsolutePath, path)
if err != nil {
return err
}
relativePaths = append(relativePaths, rel)
}
return nil
})
return relativePaths, err
}

// CloneAllRepos clones all repos
func CloneAllRepos(git git.Gitter, cloneTargets []scm.Repo) {
// Filter repos that have attributes that don't need specific scm api calls
Expand Down Expand Up @@ -1294,7 +1318,7 @@ func pruneRepos(cloneTargets []scm.Repo) int {
count := 0
colorlog.PrintInfo("\nScanning for local clones that have been removed on remote...")

files, err := os.ReadDir(outputDirAbsolutePath)
repositories, err := getRelativePathRepositories(outputDirAbsolutePath)
if err != nil {
log.Fatal(err)
}
Expand All @@ -1303,18 +1327,18 @@ func pruneRepos(cloneTargets []scm.Repo) int {
// break out of the loop.
userAgreesToDelete := true
pruneNoConfirm := os.Getenv("GHORG_PRUNE_NO_CONFIRM") == "true"
for _, f := range files {
for _, repository := range repositories {
// For each item in the org's clone directory, let's make sure we found a corresponding
// repo on the remote. We check userAgreesToDelete here too, so that if the user says
// "No" at any time, we stop trying to prune things altogether.
if userAgreesToDelete && !sliceContainsNamedRepo(cloneTargets, f.Name()) {
if userAgreesToDelete && !sliceContainsNamedRepo(cloneTargets, repository) {
// If the user specified --prune-no-confirm, we needn't prompt interactively.
userAgreesToDelete = pruneNoConfirm || interactiveYesNoPrompt(
fmt.Sprintf("%s was not found in remote. Do you want to prune it?", f.Name()))
fmt.Sprintf("%s was not found in remote. Do you want to prune it?", repository))
if userAgreesToDelete {
colorlog.PrintSubtleInfo(
fmt.Sprintf("Deleting %s", filepath.Join(outputDirAbsolutePath, f.Name())))
err = os.RemoveAll(filepath.Join(outputDirAbsolutePath, f.Name()))
fmt.Sprintf("Deleting %s", repository))
err = os.RemoveAll(filepath.Join(outputDirAbsolutePath, repository))
count++
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -1365,18 +1389,9 @@ func interactiveYesNoPrompt(prompt string) bool {
}

// There's probably a nicer way of finding whether any scm.Repo in the slice matches a given name.
// TODO, currently this does not work if user sets --preserve-dir see https://github.com/gabrie30/ghorg/issues/210 for more info
func sliceContainsNamedRepo(haystack []scm.Repo, needle string) bool {

if os.Getenv("GHORG_PRESERVE_DIRECTORY_STRUCTURE") == "true" {
colorlog.PrintError("GHORG_PRUNE (--prune) does not currently work in combination with GHORG_PRESERVE_DIRECTORY_STRUCTURE (--preserve-dir), this will come in later versions")
os.Exit(1)
}

for _, repo := range haystack {
basepath := filepath.Base(repo.Path)

if basepath == needle {
if repo.Path == needle {
return true
}
}
Expand Down
118 changes: 118 additions & 0 deletions cmd/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -525,3 +526,120 @@ func Test_filterDownReposIfTargetReposPathEnabled(t *testing.T) {
})
}
}

func TestRelativePathRepositories(t *testing.T) {
testing, err := os.MkdirTemp("", "testing")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(testing)

outputDirAbsolutePath = testing

repository := filepath.Join(testing, "repository", ".git")
if err := os.MkdirAll(repository, 0o755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}

files, err := getRelativePathRepositories(testing)
if err != nil {
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
}

if len(files) != 1 {
t.Errorf("Expected 1 directory, got %d", len(files))
}

if len(files) > 0 && files[0] != "repository" {
t.Errorf("Expected 'repository', got '%s'", files[0])
}
}

func TestRelativePathRepositoriesNoGitDir(t *testing.T) {
testing, err := os.MkdirTemp("", "testing")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(testing)

outputDirAbsolutePath = testing

directory := filepath.Join(testing, "directory")
if err := os.MkdirAll(directory, 0o755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}

files, err := getRelativePathRepositories(testing)
if err != nil {
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
}

if len(files) != 0 {
t.Errorf("Expected 0 directories, got %d", len(files))
}
}

func TestRelativePathRepositoriesWithGitSubmodule(t *testing.T) {
testing, err := os.MkdirTemp("", "testing")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(testing)

outputDirAbsolutePath = testing

repository := filepath.Join(testing, "repository", ".git")
submodule := filepath.Join(testing, "repository", "submodule", ".git")

if err := os.MkdirAll(repository, 0o755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
if err := os.MkdirAll(filepath.Dir(submodule), 0o755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
if _, err := os.Create(submodule); err != nil {
t.Fatalf("Failed to create .git file: %v", err)
}

files, err := getRelativePathRepositories(testing)
if err != nil {
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
}

if len(files) != 1 {
t.Errorf("Expected 1 directory, got %d", len(files))
}

if len(files) > 0 && files[0] != "repository" {
t.Errorf("Expected 'repository', got '%s'", files[0])
}
}

func TestRelativePathRepositoriesDeeplyNested(t *testing.T) {
testing, err := os.MkdirTemp("", "testing")
if err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
defer os.RemoveAll(testing)

outputDirAbsolutePath = testing

repository := filepath.Join(testing, "deeply", "nested", "repository", ".git")
if err := os.MkdirAll(repository, 0o755); err != nil {
t.Fatalf("Failed to create temp repository: %v", err)
}

files, err := getRelativePathRepositories(testing)
if err != nil {
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
}

if len(files) != 1 {
t.Errorf("Expected 1 directory, got %d", len(files))
}

expected := filepath.Join("deeply", "nested", "repository")
if len(files) > 0 && files[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, files[0])
}
}
15 changes: 15 additions & 0 deletions scripts/gitlab_cloud_integration_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ else
exit 1
fi

# PRUNE AND PRESERVE DIR
ghorg clone $GITLAB_GROUP --token="${GITLAB_TOKEN}" --scm=gitlab --prune --preserve-dir --prune-no-confirm
git init "${HOME}"/ghorg/"${GITLAB_GROUP}"/wayne-enterprises/wayne-industries/prunable
ghorg clone $GITLAB_GROUP --token="${GITLAB_TOKEN}" --scm=gitlab --prune --preserve-dir --prune-no-confirm

if [ -e "${HOME}"/ghorg/"${GITLAB_GROUP}"/wayne-enterprises/wayne-industries/microservice ] && \
[ ! -e "${HOME}"/ghorg/"${GITLAB_GROUP}"/wayne-enterprises/wayne-industries/prunable ]
then
echo "Pass: gitlab org clone preserve dir, prune"
rm -rf "${HOME}/ghorg/${GITLAB_GROUP}"
else
echo "Fail: gitlab org clone preserve dir, prune"
exit 1
fi

# REPO NAME COLLISION
ghorg clone $GITLAB_GROUP_2 --token="${GITLAB_TOKEN}" --scm=gitlab

Expand Down
Loading