diff --git a/pkg/git/clone.go b/pkg/git/clone.go index 789f9a04..87145f54 100644 --- a/pkg/git/clone.go +++ b/pkg/git/clone.go @@ -1,7 +1,10 @@ package git import ( + "context" + "fmt" "os" + "os/signal" "strings" "coopcloud.tech/abra/pkg/log" @@ -22,46 +25,81 @@ func gitCloneIgnoreErr(err error) bool { return false } -// Clone runs a git clone which accounts for different default branches. +// Clone runs a git clone which accounts for different default branches. This +// function respects Ctrl+C (SIGINT) calls from the user, cancelling the +// context and deleting the (typically) half-baked clone of the repository. +// This avoids broken state for future clone / recipe ops. func Clone(dir, url string) error { - if _, err := os.Stat(dir); os.IsNotExist(err) { - log.Debugf("git clone: %s", url) + ctx := context.Background() + ctx, cancelCtx := context.WithCancel(ctx) - _, err := git.PlainClone(dir, false, &git.CloneOptions{ - URL: url, - Tags: git.AllTags, - ReferenceName: plumbing.ReferenceName("refs/heads/main"), - SingleBranch: true, - }) + sigIntCh := make(chan os.Signal, 1) + signal.Notify(sigIntCh, os.Interrupt) + defer func() { + signal.Stop(sigIntCh) + cancelCtx() + }() - if err != nil && gitCloneIgnoreErr(err) { - log.Debugf("git clone: %s cloned successfully", dir) - return nil - } + errCh := make(chan error) - if err != nil { - log.Debug("git clone: main branch failed, attempting master branch") + go func() { + if _, err := os.Stat(dir); os.IsNotExist(err) { + log.Debugf("git clone: %s", url) - _, err := git.PlainClone(dir, false, &git.CloneOptions{ + _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ URL: url, Tags: git.AllTags, - ReferenceName: plumbing.ReferenceName("refs/heads/master"), + ReferenceName: plumbing.ReferenceName("refs/heads/main"), SingleBranch: true, }) if err != nil && gitCloneIgnoreErr(err) { log.Debugf("git clone: %s cloned successfully", dir) - return nil + errCh <- nil + } + + if err := ctx.Err(); err != nil { + errCh <- fmt.Errorf("git clone %s: cancelled due to interrupt", dir) } if err != nil { - return err + log.Debug("git clone: main branch failed, attempting master branch") + + _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ + URL: url, + Tags: git.AllTags, + ReferenceName: plumbing.ReferenceName("refs/heads/master"), + SingleBranch: true, + }) + + if err != nil && gitCloneIgnoreErr(err) { + log.Debugf("git clone: %s cloned successfully", dir) + errCh <- nil + } + + if err != nil { + errCh <- err + } } + + log.Debugf("git clone: %s cloned successfully", dir) + } else { + log.Debugf("git clone: %s already exists", dir) } - log.Debugf("git clone: %s cloned successfully", dir) - } else { - log.Debugf("git clone: %s already exists", dir) + errCh <- nil + }() + + select { + case <-sigIntCh: + cancelCtx() + fmt.Println() // NOTE(d1): newline after ^C + if err := os.RemoveAll(dir); err != nil { + return fmt.Errorf("unable to clean up git clone of %s: %s", dir, err) + } + return fmt.Errorf("git clone %s: cancelled due to interrupt", dir) + case err := <-errCh: + return err } return nil diff --git a/pkg/git/clone_test.go b/pkg/git/clone_test.go new file mode 100644 index 00000000..80c859eb --- /dev/null +++ b/pkg/git/clone_test.go @@ -0,0 +1,48 @@ +package git + +import ( + "fmt" + "os" + "path" + "syscall" + "testing" + + "coopcloud.tech/abra/pkg/config" +) + +func TestClone(t *testing.T) { + dir := path.Join(config.RECIPES_DIR, "gitea") + os.RemoveAll(dir) + + gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea") + if err := Clone(dir, gitURL); err != nil { + t.Fatalf("unable to git clone gitea: %s", err) + } + + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + t.Fatal("gitea repo was not cloned successfully") + } +} + +func TestCancelGitClone(t *testing.T) { + dir := path.Join(config.RECIPES_DIR, "gitea") + os.RemoveAll(dir) + + go func() { + p, err := os.FindProcess(os.Getpid()) + if err != nil { + t.Fatalf("unable to find current process: %s", err) + } + + p.Signal(syscall.SIGINT) + }() + + gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea") + if err := Clone(dir, gitURL); err == nil { + t.Fatal("cloning should have been interrupted") + } + + if _, err := os.Stat(dir); err != nil && !os.IsNotExist(err) { + t.Fatal("recipe repo was not deleted") + } +}