Skip to content

Commit 0a2bafb

Browse files
authored
feat: allow usage of --prune with --preserve-dir (#526)
1 parent 41c67cf commit 0a2bafb

File tree

4 files changed

+203
-21
lines changed

4 files changed

+203
-21
lines changed

cmd/clone.go

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bufio"
66
"crypto/sha256"
77
"fmt"
8+
"io/fs"
89
"log"
910
"net/url"
1011
"os"
@@ -556,18 +557,18 @@ func printDryRun(repos []scm.Repo) {
556557
// to do.
557558
colorlog.PrintInfo("\nScanning for local clones that have been removed on remote...")
558559

559-
files, err := os.ReadDir(outputDirAbsolutePath)
560+
repositories, err := getRelativePathRepositories(outputDirAbsolutePath)
560561
if err != nil {
561562
log.Fatal(err)
562563
}
563564

564565
eligibleForPrune := 0
565-
for _, f := range files {
566+
for _, repository := range repositories {
566567
// for each item in the org's clone directory, let's make sure we found a
567568
// corresponding repo on the remote.
568-
if !sliceContainsNamedRepo(repos, f.Name()) {
569+
if !sliceContainsNamedRepo(repos, repository) {
569570
eligibleForPrune++
570-
colorlog.PrintSubtleInfo(fmt.Sprintf("%s not found in remote.", f.Name()))
571+
colorlog.PrintSubtleInfo(fmt.Sprintf("%s not found in remote.", repository))
571572
}
572573
}
573574
colorlog.PrintSuccess(fmt.Sprintf("Local clones eligible for pruning: %d", eligibleForPrune))
@@ -599,6 +600,29 @@ func getCloneableInventory(allRepos []scm.Repo) (int, int, int, int) {
599600
return total, repos, snippets, wikis
600601
}
601602

603+
func isGitRepository(path string) bool {
604+
stat, err := os.Stat(filepath.Join(path, ".git"))
605+
return err == nil && stat.IsDir()
606+
}
607+
608+
func getRelativePathRepositories(root string) ([]string, error) {
609+
var relativePaths []string
610+
err := filepath.WalkDir(root, func(path string, file fs.DirEntry, err error) error {
611+
if err != nil {
612+
return err
613+
}
614+
if path != outputDirAbsolutePath && file.IsDir() && isGitRepository(path) {
615+
rel, err := filepath.Rel(outputDirAbsolutePath, path)
616+
if err != nil {
617+
return err
618+
}
619+
relativePaths = append(relativePaths, rel)
620+
}
621+
return nil
622+
})
623+
return relativePaths, err
624+
}
625+
602626
// CloneAllRepos clones all repos
603627
func CloneAllRepos(git git.Gitter, cloneTargets []scm.Repo) {
604628
// Filter repos that have attributes that don't need specific scm api calls
@@ -1294,7 +1318,7 @@ func pruneRepos(cloneTargets []scm.Repo) int {
12941318
count := 0
12951319
colorlog.PrintInfo("\nScanning for local clones that have been removed on remote...")
12961320

1297-
files, err := os.ReadDir(outputDirAbsolutePath)
1321+
repositories, err := getRelativePathRepositories(outputDirAbsolutePath)
12981322
if err != nil {
12991323
log.Fatal(err)
13001324
}
@@ -1303,18 +1327,18 @@ func pruneRepos(cloneTargets []scm.Repo) int {
13031327
// break out of the loop.
13041328
userAgreesToDelete := true
13051329
pruneNoConfirm := os.Getenv("GHORG_PRUNE_NO_CONFIRM") == "true"
1306-
for _, f := range files {
1330+
for _, repository := range repositories {
13071331
// For each item in the org's clone directory, let's make sure we found a corresponding
13081332
// repo on the remote. We check userAgreesToDelete here too, so that if the user says
13091333
// "No" at any time, we stop trying to prune things altogether.
1310-
if userAgreesToDelete && !sliceContainsNamedRepo(cloneTargets, f.Name()) {
1334+
if userAgreesToDelete && !sliceContainsNamedRepo(cloneTargets, repository) {
13111335
// If the user specified --prune-no-confirm, we needn't prompt interactively.
13121336
userAgreesToDelete = pruneNoConfirm || interactiveYesNoPrompt(
1313-
fmt.Sprintf("%s was not found in remote. Do you want to prune it?", f.Name()))
1337+
fmt.Sprintf("%s was not found in remote. Do you want to prune it?", repository))
13141338
if userAgreesToDelete {
13151339
colorlog.PrintSubtleInfo(
1316-
fmt.Sprintf("Deleting %s", filepath.Join(outputDirAbsolutePath, f.Name())))
1317-
err = os.RemoveAll(filepath.Join(outputDirAbsolutePath, f.Name()))
1340+
fmt.Sprintf("Deleting %s", repository))
1341+
err = os.RemoveAll(filepath.Join(outputDirAbsolutePath, repository))
13181342
count++
13191343
if err != nil {
13201344
log.Fatal(err)
@@ -1365,18 +1389,9 @@ func interactiveYesNoPrompt(prompt string) bool {
13651389
}
13661390

13671391
// There's probably a nicer way of finding whether any scm.Repo in the slice matches a given name.
1368-
// TODO, currently this does not work if user sets --preserve-dir see https://github.com/gabrie30/ghorg/issues/210 for more info
13691392
func sliceContainsNamedRepo(haystack []scm.Repo, needle string) bool {
1370-
1371-
if os.Getenv("GHORG_PRESERVE_DIRECTORY_STRUCTURE") == "true" {
1372-
colorlog.PrintError("GHORG_PRUNE (--prune) does not currently work in combination with GHORG_PRESERVE_DIRECTORY_STRUCTURE (--preserve-dir), this will come in later versions")
1373-
os.Exit(1)
1374-
}
1375-
13761393
for _, repo := range haystack {
1377-
basepath := filepath.Base(repo.Path)
1378-
1379-
if basepath == needle {
1394+
if repo.Path == fmt.Sprintf("/%s", needle) {
13801395
return true
13811396
}
13821397
}

cmd/clone_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"log"
66
"os"
7+
"path/filepath"
78
"reflect"
89
"strings"
910
"testing"
@@ -525,3 +526,154 @@ func Test_filterDownReposIfTargetReposPathEnabled(t *testing.T) {
525526
})
526527
}
527528
}
529+
530+
func TestRelativePathRepositories(t *testing.T) {
531+
testing, err := os.MkdirTemp("", "testing")
532+
if err != nil {
533+
t.Fatalf("Failed to create temp directory: %v", err)
534+
}
535+
defer os.RemoveAll(testing)
536+
537+
outputDirAbsolutePath = testing
538+
539+
repository := filepath.Join(testing, "repository", ".git")
540+
if err := os.MkdirAll(repository, 0o755); err != nil {
541+
t.Fatalf("Failed to create directory: %v", err)
542+
}
543+
544+
files, err := getRelativePathRepositories(testing)
545+
if err != nil {
546+
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
547+
}
548+
549+
if len(files) != 1 {
550+
t.Errorf("Expected 1 directory, got %d", len(files))
551+
}
552+
553+
if len(files) > 0 && files[0] != "repository" {
554+
t.Errorf("Expected 'repository', got '%s'", files[0])
555+
}
556+
}
557+
558+
func TestRelativePathRepositoriesNoGitDir(t *testing.T) {
559+
testing, err := os.MkdirTemp("", "testing")
560+
if err != nil {
561+
t.Fatalf("Failed to create temp directory: %v", err)
562+
}
563+
defer os.RemoveAll(testing)
564+
565+
outputDirAbsolutePath = testing
566+
567+
directory := filepath.Join(testing, "directory")
568+
if err := os.MkdirAll(directory, 0o755); err != nil {
569+
t.Fatalf("Failed to create directory: %v", err)
570+
}
571+
572+
files, err := getRelativePathRepositories(testing)
573+
if err != nil {
574+
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
575+
}
576+
577+
if len(files) != 0 {
578+
t.Errorf("Expected 0 directories, got %d", len(files))
579+
}
580+
}
581+
582+
func TestRelativePathRepositoriesWithGitSubmodule(t *testing.T) {
583+
testing, err := os.MkdirTemp("", "testing")
584+
if err != nil {
585+
t.Fatalf("Failed to create temp directory: %v", err)
586+
}
587+
defer os.RemoveAll(testing)
588+
589+
outputDirAbsolutePath = testing
590+
591+
repository := filepath.Join(testing, "repository", ".git")
592+
submodule := filepath.Join(testing, "repository", "submodule", ".git")
593+
594+
if err := os.MkdirAll(repository, 0o755); err != nil {
595+
t.Fatalf("Failed to create directory: %v", err)
596+
}
597+
if err := os.MkdirAll(filepath.Dir(submodule), 0o755); err != nil {
598+
t.Fatalf("Failed to create directory: %v", err)
599+
}
600+
if _, err := os.Create(submodule); err != nil {
601+
t.Fatalf("Failed to create .git file: %v", err)
602+
}
603+
604+
files, err := getRelativePathRepositories(testing)
605+
if err != nil {
606+
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
607+
}
608+
609+
if len(files) != 1 {
610+
t.Errorf("Expected 1 directory, got %d", len(files))
611+
}
612+
613+
if len(files) > 0 && files[0] != "repository" {
614+
t.Errorf("Expected 'repository', got '%s'", files[0])
615+
}
616+
}
617+
618+
func TestRelativePathRepositoriesDeeplyNested(t *testing.T) {
619+
testing, err := os.MkdirTemp("", "testing")
620+
if err != nil {
621+
t.Fatalf("Failed to create directory: %v", err)
622+
}
623+
defer os.RemoveAll(testing)
624+
625+
outputDirAbsolutePath = testing
626+
627+
repository := filepath.Join(testing, "deeply", "nested", "repository", ".git")
628+
if err := os.MkdirAll(repository, 0o755); err != nil {
629+
t.Fatalf("Failed to create repository: %v", err)
630+
}
631+
632+
files, err := getRelativePathRepositories(testing)
633+
if err != nil {
634+
t.Fatalf("getRelativePathRepositories returned an error: %v", err)
635+
}
636+
637+
if len(files) != 1 {
638+
t.Errorf("Expected 1 directory, got %d", len(files))
639+
}
640+
641+
expected := filepath.Join("deeply", "nested", "repository")
642+
if len(files) > 0 && files[0] != expected {
643+
t.Errorf("Expected '%s', got '%s'", expected, files[0])
644+
}
645+
}
646+
647+
func TestPruneRepos(t *testing.T) {
648+
os.Setenv("GHORG_PRUNE_NO_CONFIRM", "true")
649+
650+
cloneTargets := []scm.Repo{{Path: "/repository"}}
651+
652+
testing, err := os.MkdirTemp("", "testing")
653+
if err != nil {
654+
t.Fatalf("Failed to create directory: %v", err)
655+
}
656+
defer os.RemoveAll(testing)
657+
658+
outputDirAbsolutePath = testing
659+
660+
repository := filepath.Join(testing, "repository", ".git")
661+
if err := os.MkdirAll(repository, 0o755); err != nil {
662+
t.Fatalf("Failed to create repository: %v", err)
663+
}
664+
665+
prunable := filepath.Join(testing, "prunnable", ".git")
666+
if err := os.MkdirAll(prunable, 0o755); err != nil {
667+
t.Fatalf("Failed to create directory: %v", err)
668+
}
669+
670+
pruneRepos(cloneTargets)
671+
672+
if _, err := os.Stat(repository); os.IsNotExist(err) {
673+
t.Errorf("Expected '%s' to exist, but it was deleted", repository)
674+
}
675+
676+
if _, err := os.Stat(prunable); !os.IsNotExist(err) {
677+
t.Errorf("Expected '%s' to be deleted, but it exists", prunable)
678+
}
679+
}

scm/structs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type Repo struct {
88
Name string
99
// HostPath is the path on the users machine that the repo will be cloned to. Its used in all the git commands to locate the directory of the repo. HostPath is updated for wikis and snippets because the folder for the clone is appended with .wiki and .snippet
1010
HostPath string
11-
// Path where the repo is located within the scm provider. Its mostly used with gitlab repos when the directory structure is preserved. In this case the path becomes where to locate the repo in relation to gitlab.com/group/group/group/repo.git => group/group/group/repo
11+
// Path where the repo is located within the scm provider. Its mostly used with gitlab repos when the directory structure is preserved. In this case the path becomes where to locate the repo in relation to gitlab.com/group/group/group/repo.git => /group/group/group/repo
1212
Path string
1313
// URL is the web address of the repo
1414
URL string

scripts/gitlab_cloud_integration_tests.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ else
8787
exit 1
8888
fi
8989

90+
# PRUNE AND PRESERVE DIR
91+
ghorg clone $GITLAB_GROUP --token="${GITLAB_TOKEN}" --scm=gitlab --prune --prune-no-confirm --preserve-dir
92+
git init "${HOME}"/ghorg/"${GITLAB_GROUP}"/wayne-enterprises/wayne-industries/prunable
93+
ghorg clone $GITLAB_GROUP --token="${GITLAB_TOKEN}" --scm=gitlab --prune --prune-no-confirm --preserve-dir
94+
95+
if [ -e "${HOME}"/ghorg/"${GITLAB_GROUP}"/wayne-enterprises/wayne-industries/microservice ] && \
96+
[ ! -e "${HOME}"/ghorg/"${GITLAB_GROUP}"/wayne-enterprises/wayne-industries/prunable ]
97+
then
98+
echo "Pass: gitlab org clone preserve dir, prune"
99+
rm -rf "${HOME}/ghorg/${GITLAB_GROUP}"
100+
else
101+
echo "Fail: gitlab org clone preserve dir, prune"
102+
exit 1
103+
fi
104+
90105
# REPO NAME COLLISION
91106
ghorg clone $GITLAB_GROUP_2 --token="${GITLAB_TOKEN}" --scm=gitlab
92107

0 commit comments

Comments
 (0)