Repository: jenkins-x/lighthouse
Pull request #1467 (2022-07-27 07:09:44)

feat: Sparse checkout
Sparse checkout is implemented especially for handling webhooks.

I have also changed to shallow clones even if you don't opt in to sparse checkout.

For now sparse checkout is enabled by setting the environment variable SPARSE_CHECKOUT=true.

I'm in the process of testing this in our own dev cluster now.

This is a part of solving jenkins-x/jx#8240



Patch:
diff --git a/pkg/filebrowser/fake/fake_file_browser.go b/pkg/filebrowser/fake/fake_file_browser.go
index e686c303..95731fc7 100644
--- a/pkg/filebrowser/fake/fake_file_browser.go
+++ b/pkg/filebrowser/fake/fake_file_browser.go
@@ -63,7 +63,7 @@ func (f *fakeFileBrowser) getPath(owner, repo, path, ref string) string {
 	return filepath.Join(f.dir, path)
 }
 
-func (f *fakeFileBrowser) WithDir(owner, repo, ref string, fc filebrowser.FetchCache, fn func(dir string) error) error {
+func (f *fakeFileBrowser) WithDir(owner, repo, ref string, fc filebrowser.FetchCache, sparseCheckoutPatterns []string, fn func(dir string) error) error {
 	dir := f.getPath(owner, repo, "", ref)
 	return fn(dir)
 }
diff --git a/pkg/filebrowser/git_file_browser.go b/pkg/filebrowser/git_file_browser.go
index c52daed1..e6fbd0e6 100644
--- a/pkg/filebrowser/git_file_browser.go
+++ b/pkg/filebrowser/git_file_browser.go
@@ -2,9 +2,11 @@ package filebrowser
 
 import (
 	"io/ioutil"
+	"os"
 	"os/exec"
 	"path/filepath"
 	"regexp"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -38,8 +40,8 @@ func NewFileBrowserFromGitClient(clientFactory git.ClientFactory) Interface {
 	}
 }
 
-func (f *gitFileBrowser) WithDir(owner, repo, ref string, fc FetchCache, fn func(dir string) error) error {
-	return f.withRepoClient(owner, repo, ref, fc, func(repoClient git.RepoClient) error {
+func (f *gitFileBrowser) WithDir(owner, repo, ref string, fc FetchCache, sparseCheckoutPatterns []string, fn func(dir string) error) error {
+	return f.withRepoClient(owner, repo, ref, fc, sparseCheckoutPatterns, func(repoClient git.RepoClient) error {
 		dir := repoClient.Directory()
 		return fn(dir)
 	})
@@ -50,7 +52,7 @@ func (f *gitFileBrowser) GetMainAndCurrentBranchRefs(_, _, eventRef string) ([]s
 }
 
 func (f *gitFileBrowser) GetFile(owner, repo, path, ref string, fc FetchCache) (answer []byte, err error) {
-	err = f.withRepoClient(owner, repo, ref, fc, func(repoClient git.RepoClient) error {
+	err = f.withRepoClient(owner, repo, ref, fc, []string{"/" + path}, func(repoClient git.RepoClient) error {
 		f := repoPath(repoClient, path)
 		exists, err := util.FileExists(f)
 		if err != nil {
@@ -67,7 +69,7 @@ func (f *gitFileBrowser) GetFile(owner, repo, path, ref string, fc FetchCache) (
 }
 
 func (f *gitFileBrowser) ListFiles(owner, repo, path, ref string, fc FetchCache) (answer []*scm.FileEntry, err error) {
-	err = f.withRepoClient(owner, repo, ref, fc, func(repoClient git.RepoClient) error {
+	err = f.withRepoClient(owner, repo, ref, fc, []string{"/" + path + "/*"}, func(repoClient git.RepoClient) error {
 		dir := repoPath(repoClient, path)
 		exists, err := util.DirExists(dir)
 		if err != nil {
@@ -112,18 +114,21 @@ func repoPath(repoClient git.RepoClient, path string) string {
 	return filepath.Join(dir, path)
 }
 
-func (f *gitFileBrowser) withRepoClient(owner, repo, ref string, fc FetchCache, fn func(repoClient git.RepoClient) error) error {
+func (f *gitFileBrowser) withRepoClient(owner, repo, ref string, fc FetchCache, sparseCheckoutPatterns []string, fn func(repoClient git.RepoClient) error) error {
 	client := f.getOrCreateClient(owner, repo)
-
-	var repoClient git.RepoClient
+	sparseCheckout, _ := strconv.ParseBool(os.Getenv("SPARSE_CHECKOUT"))
 	var err error
 	client.lock.Lock()
 	defer client.lock.Unlock()
 	if client.repoClient == nil {
-		client.repoClient, err = f.clientFactory.ClientFor(owner, repo)
+		client.repoClient, err = f.clientFactory.ClientFor(owner, repo, sparseCheckoutPatterns)
 		if err != nil {
 			return errors.Wrapf(err, "failed to create repo client")
 		}
+	} else if sparseCheckout && len(sparseCheckoutPatterns) > 0 {
+		if err := client.repoClient.SetSparseCheckoutPatterns(sparseCheckoutPatterns); err != nil {
+			return errors.Wrapf(err, "failed to set sparse checkout patterns")
+		}
 	}
 	if client.mainBranch == "" {
 		var err error
@@ -133,13 +138,12 @@ func (f *gitFileBrowser) withRepoClient(owner, repo, ref string, fc FetchCache,
 		}
 	}
 	if err == nil {
-		repoClient = client.repoClient
 		err = client.UseRef(ref, fc)
 		if err != nil {
 			err = errors.Wrapf(err, "failed to switch to ref %s", ref)
 		}
 		if err == nil {
-			err = fn(repoClient)
+			err = fn(client.repoClient)
 			if err != nil {
 				err = errors.Wrapf(err, "failed to process repo %s/%s refref %s", owner, repo, ref)
 			}
@@ -230,10 +234,10 @@ func (c *repoClientFacade) UseRef(ref string, fc FetchCache) error {
 			}
 		}
 		if !isSHA {
-			// lets pull any new changes into the main branch
-			err := c.repoClient.Pull()
+			// lets merge any new changes into the main branch
+			_, err := runCmd(c.repoClient.Directory(), "git", "merge", "FETCH_HEAD")
 			if err != nil {
-				return errors.Wrapf(err, "failed to fetch repository %s", c.fullName)
+				return errors.Wrapf(err, "failed to merge repository %s", c.fullName)
 			}
 		}
 	}
diff --git a/pkg/filebrowser/interface.go b/pkg/filebrowser/interface.go
index 46b3b21d..79f58c10 100644
--- a/pkg/filebrowser/interface.go
+++ b/pkg/filebrowser/interface.go
@@ -14,5 +14,5 @@ type Interface interface {
 	ListFiles(owner, repo, path, ref string, fc FetchCache) ([]*scm.FileEntry, error)
 
 	// WithDir processes the given repository and reference at the given directory
-	WithDir(owner, repo, ref string, fc FetchCache, f func(dir string) error) error
+	WithDir(owner, repo, ref string, fc FetchCache, sparseCheckoutPatterns []string, f func(dir string) error) error
 }
diff --git a/pkg/git/git.go b/pkg/git/git.go
index d04cfcff..b146ef21 100644
--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -25,6 +25,7 @@ type Client interface {
 	SetRemote(remote string)
 	SetCredentials(user string, tokenGenerator func() []byte)
 	Clone(repo string) (*Repo, error)
+	SparseClone(repo string, sparseCheckoutPatterns []string) (*Repo, error)
 }
 
 // client can clone repos. It keeps a local cache, so successive clones of the
@@ -150,18 +151,8 @@ func (c *client) Clone(repo string) (*Repo, error) {
 		if err := os.Mkdir(filepath.Dir(cache), os.ModePerm); err != nil && !os.IsExist(err) {
 			return nil, err
 		}
-		prefix := ""
-		repoText := repo
-		if c.gitKind == kindBitbucketServer {
-			prefix = "scm/"
-			idx := strings.Index(repo, "/")
-
-			// to clone on bitbucket we need to lower case the projectKey owner
-			if idx > 0 {
-				repoText = fmt.Sprintf("%s/%s", strings.ToLower(repo[0:idx]), repo[idx+1:])
-			}
-		}
-		remote := fmt.Sprintf("%s/%s%s", base, prefix, repoText)
+		remote := getRemote(repo, c, base)
+
 		if b, err := retryCmd(c.logger, "", c.git, "clone", "--mirror", remote, cache); err != nil {
 			return nil, fmt.Errorf("git cache clone error: %v. output: %s", err, string(b))
 		}
@@ -193,6 +184,69 @@ func (c *client) Clone(repo string) (*Repo, error) {
 	}, nil
 }
 
+func (c *client) SparseClone(repo string, sparseCheckoutPatterns []string) (*Repo, error) {
+	// Make pattern part of cache key somehow.
+	replacer := strings.NewReplacer(".", "_", "/", "_")
+	cacheKey := repo + replacer.Replace(strings.Join(sparseCheckoutPatterns, "-"))
+	c.lockRepo(cacheKey)
+	defer c.unlockRepo(cacheKey)
+
+	base := c.base
+	user, pass := c.getCredentials()
+	if user != "" && pass != "" {
+		host, scheme := gitHostAndScheme(c.base)
+		base = fmt.Sprintf("%s://%s:%s@%s", scheme, user, pass, host)
+	}
+	cache := filepath.Join(c.dir, cacheKey)
+	if _, err := os.Stat(cache); os.IsNotExist(err) {
+		// Cache miss, clone it now.
+		c.logger.Infof("Cloning %s for the first time.", repo)
+		if err := os.Mkdir(filepath.Dir(cache), os.ModePerm); err != nil && !os.IsExist(err) {
+			return nil, err
+		}
+		remote := getRemote(repo, c, base)
+
+		if b, err := retryCmd(c.logger, "", c.git, "clone", "--no-checkout", "--filter=blob:none", "--sparse", "--depth=1", remote, cache); err != nil {
+			return nil, fmt.Errorf("failed to clone repository: %v. output: %s", err, string(b))
+		}
+		if b, err := retryCmd(c.logger, cache, c.git, append([]string{"sparse-checkout", "set"}, sparseCheckoutPatterns...)...); err != nil {
+			return nil, fmt.Errorf("failed to set sparse checkout patterns to %v: %v. output: %s", sparseCheckoutPatterns, err, string(b))
+		}
+	} else if err != nil {
+		return nil, err
+	} else {
+		// Cache hit. Do a git fetch to keep updated.
+		c.logger.Infof("Fetching %s.", repo)
+		if b, err := retryCmd(c.logger, cache, c.git, "fetch", "--depth=1"); err != nil {
+			return nil, fmt.Errorf("git fetch error: %v. output: %s", err, string(b))
+		}
+	}
+	return &Repo{
+		Dir:    cache,
+		logger: c.logger,
+		git:    c.git,
+		base:   base,
+		repo:   repo,
+		user:   user,
+		pass:   pass,
+	}, nil
+}
+
+func getRemote(repo string, c *client, base string) string {
+	prefix := ""
+	repoText := repo
+	if c.gitKind == kindBitbucketServer {
+		prefix = "scm/"
+		idx := strings.Index(repo, "/")
+
+		// to clone on bitbucket we need to lower case the projectKey owner
+		if idx > 0 {
+			repoText = fmt.Sprintf("%s/%s", strings.ToLower(repo[0:idx]), repo[idx+1:])
+		}
+	}
+	return fmt.Sprintf("%s/%s%s", base, prefix, repoText)
+}
+
 func gitHostAndScheme(s string) (string, string) {
 	u, err := url.Parse(s)
 	if err == nil {
@@ -359,6 +413,7 @@ func retryCmd(l *logrus.Entry, dir, cmd string, arg ...string) ([]byte, error) {
 			sleepyTime *= 2
 			continue
 		}
+		l.Debugf("Running %s %v succeeded with output %s.", cmd, arg, string(b))
 		break
 	}
 	return b, err
diff --git a/pkg/git/v2/client_factory.go b/pkg/git/v2/client_factory.go
index 3d47a5f9..f4f750c5 100644
--- a/pkg/git/v2/client_factory.go
+++ b/pkg/git/v2/client_factory.go
@@ -21,6 +21,7 @@ import (
 	"os"
 	"path"
 	"runtime"
+	"strconv"
 	"sync"
 	"time"
 
@@ -34,7 +35,7 @@ type ClientFactory interface {
 	// been cloned to the given directory.
 	ClientFromDir(org, repo, dir string) (RepoClient, error)
 	// ClientFor creates a client that operates on a new clone of the repo.
-	ClientFor(org, repo string) (RepoClient, error)
+	ClientFor(org, repo string, sparseCheckoutPatterns []string) (RepoClient, error)
 
 	// Clean removes the caches used to generate clients
 	Clean() error
@@ -245,7 +246,7 @@ func (c *clientFactory) ClientFromDir(org, repo, dir string) (RepoClient, error)
 // In that case, it must do a full git mirror clone. For large repos, this can
 // take a while. Once that is done, it will do a git fetch instead of a clone,
 // which will usually take at most a few seconds.
-func (c *clientFactory) ClientFor(org, repo string) (RepoClient, error) {
+func (c *clientFactory) ClientFor(org, repo string, sparseCheckoutPatterns []string) (RepoClient, error) {
 	start := time.Now()
 	cacheDir := path.Join(c.cacheDir, org, repo)
 	l := c.logger.WithFields(logrus.Fields{"org": org, "repo": repo, "dir": cacheDir})
@@ -292,6 +293,12 @@ func (c *clientFactory) ClientFor(org, repo string) (RepoClient, error) {
 	if err := repoClientCloner.Clone(cacheDir); err != nil {
 		return nil, err
 	}
+	sparseCheckout, _ := strconv.ParseBool(os.Getenv("SPARSE_CHECKOUT"))
+	if sparseCheckout && len(sparseCheckoutPatterns) > 0 {
+		if err := repoClient.SetSparseCheckoutPatterns(sparseCheckoutPatterns); err != nil {
+			return nil, err
+		}
+	}
 
 	duration := time.Now().Sub(start)
 	l.WithField("Duration", duration.String()).Debug("cloned repository")
diff --git a/pkg/git/v2/interactor.go b/pkg/git/v2/interactor.go
index a12a54f4..feec1f12 100644
--- a/pkg/git/v2/interactor.go
+++ b/pkg/git/v2/interactor.go
@@ -22,6 +22,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"strconv"
 	"strings"
 
 	"github.com/sirupsen/logrus"
@@ -70,6 +71,8 @@ type Interactor interface {
 	HasSHA(ref string) (string, error)
 	// Pull pulls any changes into a branch from the remote
 	Pull() error
+	// SetSparseCheckoutPatterns initialises att set patterns for sparse checkout
+	SetSparseCheckoutPatterns(sparseCheckoutPatterns []string) error
 }
 
 // cacher knows how to cache and update repositories in a central cache
@@ -109,15 +112,30 @@ func (i *interactor) Clean() error {
 	return os.RemoveAll(i.dir)
 }
 
-// Clone clones the repository from a local path.
-func (i *interactor) Clone(from string) error {
-	i.logger.Debugf("Creating a clone of the repo at %s from %s", i.dir, from)
-	if out, err := i.executor.Run("clone", from, i.dir); err != nil {
+// Clone clones the repository from a repository.
+func (i *interactor) Clone(repo string) error {
+	sparseCheckout, _ := strconv.ParseBool(os.Getenv("SPARSE_CHECKOUT"))
+	i.logger.Debugf("Creating a clone of the repo at %s from %s", i.dir, repo)
+	args := []string{"clone", "--no-checkout", "--depth=1"}
+	if sparseCheckout {
+		args = append(args, "--filter=blob:none")
+	}
+	if out, err := i.executor.Run(append(args, repo, i.dir)...); err != nil {
 		return fmt.Errorf("error creating a clone: %v %v", err, string(out))
 	}
 	return nil
 }
 
+func (i *interactor) SetSparseCheckoutPatterns(sparseCheckoutPatterns []string) error {
+	if out, err := i.executor.Run("sparse-checkout", "init"); err != nil {
+		return fmt.Errorf("failed to init sparse checkout: %v. output: %s", err, string(out))
+	}
+	if out, err := i.executor.Run(append([]string{"sparse-checkout", "set"}, sparseCheckoutPatterns...)...); err != nil {
+		return fmt.Errorf("failed to set sparse checkout patterns to %v: %v. output: %s", sparseCheckoutPatterns, err, string(out))
+	}
+	return nil
+}
+
 // MirrorClone sets up a mirror of the source repository.
 func (i *interactor) MirrorClone() error {
 	i.logger.Debugf("Creating a mirror of the repo at %s", i.dir)
@@ -125,7 +143,7 @@ func (i *interactor) MirrorClone() error {
 	if err != nil {
 		return fmt.Errorf("could not resolve remote for cloning: %v", err)
 	}
-	out, err := i.executor.Run("clone", "--mirror", remote, i.dir)
+	out, err := i.executor.Run("clone", "--mirror", "--depth=1", remote, i.dir)
 	if err != nil {
 		// the returned error is not being reported
 		i.logger.Errorf("error creating a mirror clone: %v %v", err, string(out))
diff --git a/pkg/git/v2/no_mirror_client_factory.go b/pkg/git/v2/no_mirror_client_factory.go
index 4d1ff2b9..7d62f944 100644
--- a/pkg/git/v2/no_mirror_client_factory.go
+++ b/pkg/git/v2/no_mirror_client_factory.go
@@ -3,7 +3,7 @@ package git
 import (
 	"io/ioutil"
 	"os"
-	"path"
+	"strconv"
 	"sync"
 	"time"
 
@@ -109,50 +109,28 @@ func (c *noMirrorClientFactory) ClientFromDir(org, repo, dir string) (RepoClient
 }
 
 // ClientFor returns a repository client for the specified repository.
-// This function may take a long time if it is the first time cloning the repo.
-// In that case, it must do a full git mirror clone. For large repos, this can
-// take a while. Once that is done, it will do a git fetch instead of a clone,
-// which will usually take at most a few seconds.
-func (c *noMirrorClientFactory) ClientFor(org, repo string) (RepoClient, error) {
+func (c *noMirrorClientFactory) ClientFor(org, repo string, sparseCheckoutPatterns []string) (RepoClient, error) {
 	start := time.Now()
-	cacheDir := path.Join(c.cacheDir, org, repo)
-	l := c.logger.WithFields(logrus.Fields{"org": org, "repo": repo, "dir": cacheDir})
-	l.Debug("Creating a client from the cache.")
-
 	repoDir, err := ioutil.TempDir(c.cacheDirBase, "gitrepo")
 	if err != nil {
 		return nil, err
 	}
+	l := c.logger.WithFields(logrus.Fields{"org": org, "repo": repo, "dir": repoDir})
+	l.Debug("Creating a client.")
 	_, repoClientCloner, repoClient, err := c.bootstrapClients(org, repo, repoDir)
 	if err != nil {
 		return nil, err
 	}
-	c.masterLock.Lock()
-	if _, exists := c.repoLocks[cacheDir]; !exists {
-		c.repoLocks[cacheDir] = &sync.Mutex{}
+	remote, err := c.remotes.CentralRemote(org, repo)()
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to resolve remote for %s/%s", org, repo)
 	}
-	c.masterLock.Unlock()
-	c.repoLocks[cacheDir].Lock()
-	defer c.repoLocks[cacheDir].Unlock()
-	if _, err := os.Stat(path.Join(cacheDir, "HEAD")); os.IsNotExist(err) {
-		// we have not yet cloned this repo, we need to do a full clone
-		if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil && !os.IsExist(err) {
-			return nil, err
-		}
-
-		remote, err := c.remotes.CentralRemote(org, repo)()
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to resolve remote for %s/%s", org, repo)
-		}
-		if err := repoClientCloner.Clone(remote); err != nil {
-			return nil, err
-		}
-	} else if err != nil {
-		// something unexpected happened
+	if err := repoClientCloner.Clone(remote); err != nil {
 		return nil, err
-	} else {
-		// we have cloned the repo previously, but will refresh it
-		if err := repoClient.Fetch(); err != nil {
+	}
+	sparseCheckout, _ := strconv.ParseBool(os.Getenv("SPARSE_CHECKOUT"))
+	if sparseCheckout && len(sparseCheckoutPatterns) > 0 {
+		if err := repoClient.SetSparseCheckoutPatterns(sparseCheckoutPatterns); err != nil {
 			return nil, err
 		}
 	}
diff --git a/pkg/plugins/approve/approvers/owners.go b/pkg/plugins/approve/approvers/owners.go
index 5acb1f83..3d6c37e5 100644
--- a/pkg/plugins/approve/approvers/owners.go
+++ b/pkg/plugins/approve/approvers/owners.go
@@ -58,7 +58,7 @@ type Owners struct {
 
 // NewOwners consturcts a new Owners instance. filenames is the slice of files changed.
 func NewOwners(log *logrus.Entry, filenames []string, r Repo, s int64) Owners {
-	log.Warnf("OWNERS FILENAMES : %s", filenames)
+	log.Debugf("OWNERS FILENAMES : %s", filenames)
 	return Owners{filenames: filenames, repo: r, seed: s, log: log}
 }
 
@@ -386,9 +386,9 @@ func (ap Approvers) GetNoIssueApproversSet() sets.String {
 func (ap Approvers) GetFilesApprovers() map[string]sets.String {
 	filesApprovers := map[string]sets.String{}
 	currentApprovers := ap.GetCurrentApproversSetCased()
-	logrus.Warnf("CURRENT APPROVE SET CASED: %s", currentApprovers.List())
+	logrus.Tracef("CURRENT APPROVE SET CASED: %s", currentApprovers.List())
 	for fn, potentialApprovers := range ap.owners.GetApprovers() {
-		logrus.Warnf("POTENTIAL APPROVERS: %s", potentialApprovers.List())
+		logrus.Tracef("POTENTIAL APPROVERS: %s", potentialApprovers.List())
 		// The order of parameter matters here:
 		// - currentApprovers is the list of github handles that have approved
 		// - potentialApprovers is the list of handles in the OWNER
diff --git a/pkg/plugins/trigger/trigger.go b/pkg/plugins/trigger/trigger.go
index 8221ee76..7db1c649 100644
--- a/pkg/plugins/trigger/trigger.go
+++ b/pkg/plugins/trigger/trigger.go
@@ -302,7 +302,13 @@ func skipRequested(c Client, pr *scm.PullRequest, skippedJobs []job.Presubmit) e
 			continue
 		}
 		c.Logger.Infof("Skipping %s build.", job.Name)
-		if _, err := c.SCMProviderClient.CreateStatus(pr.Base.Repo.Namespace, pr.Base.Repo.Name, pr.Head.Ref, skippedStatusFor(job.Context)); err != nil {
+		status := skippedStatusFor(job.Context)
+		if _, err := c.SCMProviderClient.CreateStatus(pr.Base.Repo.Namespace, pr.Base.Repo.Name, pr.Head.Ref, status); err != nil {
+			c.Logger.WithError(err).
+				WithField("status", status).
+				WithField("repo", pr.Base.Repo.Namespace+"/"+pr.Base.Repo.Name).
+				WithField("head", pr.Head.Ref).
+				Warnf("Failed creating status for build %s", job.Name)
 			errors = append(errors, err)
 		}
 	}
diff --git a/pkg/poller/poller.go b/pkg/poller/poller.go
index 5791cc24..e2272bb3 100644
--- a/pkg/poller/poller.go
+++ b/pkg/poller/poller.go
@@ -16,7 +16,7 @@ import (
 )
 
 var (
-	censor = func(content []byte) []byte { return content }
+	censor           = func(content []byte) []byte { return content }
 	StatusesPageSize = 100
 )
 
@@ -85,7 +85,7 @@ func (c *pollingController) PollReleases() {
 		ref := ""
 		sha := ""
 		fc := filebrowser.NewFetchCache()
-		err := c.fb.WithDir(owner, repo, ref, fc, func(dir string) error {
+		err := c.fb.WithDir(owner, repo, ref, fc, nil, func(dir string) error {
 			executor, err := git.NewCensoringExecutor(dir, censor, l)
 			if err != nil {
 				return errors.Wrapf(err, "failed to create git executor")
diff --git a/pkg/repoowners/repoowners.go b/pkg/repoowners/repoowners.go
index 74428dc4..00214339 100644
--- a/pkg/repoowners/repoowners.go
+++ b/pkg/repoowners/repoowners.go
@@ -22,6 +22,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"strconv"
 	"strings"
 	"sync"
 
@@ -221,6 +222,7 @@ func (c *Client) LoadRepoOwners(org, repo, base string) (RepoOwner, error) {
 	cloneRef := fmt.Sprintf("%s/%s", org, repo)
 	fullName := fmt.Sprintf("%s:%s", cloneRef, base)
 	mdYaml := c.mdYAMLEnabled(org, repo)
+	sparseCheckout, _ := strconv.ParseBool(os.Getenv("SPARSE_CHECKOUT"))
 
 	sha, err := c.spc.GetRef(org, repo, fmt.Sprintf("heads/%s", base))
 	if err != nil {
@@ -231,11 +233,20 @@ func (c *Client) LoadRepoOwners(org, repo, base string) (RepoOwner, error) {
 	defer c.lock.Unlock()
 	entry, ok := c.cache[fullName]
 	if !ok || entry.sha != sha || entry.owners == nil || entry.owners.enableMDYAML != mdYaml {
-		gitRepo, err := c.git.Clone(cloneRef)
+		var gitRepo *git2.Repo
+		if sparseCheckout {
+			sparseCheckoutPatterns := []string{"/OWNERS_ALIASES", "OWNERS"}
+			if mdYaml {
+				sparseCheckoutPatterns = append(sparseCheckoutPatterns, "*.md")
+			}
+			gitRepo, err = c.git.SparseClone(cloneRef, sparseCheckoutPatterns)
+		} else {
+			gitRepo, err = c.git.Clone(cloneRef)
+			defer gitRepo.Clean()
+		}
 		if err != nil {
 			return nil, fmt.Errorf("failed to clone %s: %v", cloneRef, err)
 		}
-		defer gitRepo.Clean()
 		if err := gitRepo.Checkout(base); err != nil {
 			return nil, err
 		}
diff --git a/pkg/triggerconfig/inrepo/load_triggers.go b/pkg/triggerconfig/inrepo/load_triggers.go
index 07a39791..8a08118d 100644
--- a/pkg/triggerconfig/inrepo/load_triggers.go
+++ b/pkg/triggerconfig/inrepo/load_triggers.go
@@ -43,7 +43,7 @@ func MergeTriggers(cfg *config.Config, pluginCfg *plugins.Configuration, fileBro
 // LoadTriggerConfig loads the `lighthouse.yaml` configuration files in the repository
 func LoadTriggerConfig(fileBrowsers *filebrowser.FileBrowsers, fc filebrowser.FetchCache, cache *ResolverCache, ownerName string, repoName string, sha string) (*triggerconfig.Config, error) {
 	var answer *triggerconfig.Config
-	err := fileBrowsers.LighthouseGitFileBrowser().WithDir(ownerName, repoName, sha, fc, func(dir string) error {
+	err := fileBrowsers.LighthouseGitFileBrowser().WithDir(ownerName, repoName, sha, fc, []string{"/.lighthouse/**"}, func(dir string) error {
 		path := filepath.Join(dir, ".lighthouse")
 		exists, err := util.DirExists(path)
 		if err != nil {
diff --git a/pkg/webhook/events.go b/pkg/webhook/events.go
index bfde8367..567475a4 100644
--- a/pkg/webhook/events.go
+++ b/pkg/webhook/events.go
@@ -249,7 +249,7 @@ func (s *Server) handlePullRequestEvent(l *logrus.Entry, pr *scm.PullRequestHook
 				go func(p string, h plugins.PullRequestHandler) {
 					defer s.wg.Done()
 					if err := h(agent, *pr); err != nil {
-						agent.Logger.WithError(err).Error("Error handling PullRequestEvent.")
+						agent.Logger.WithField("plugin", p).WithError(err).Error("Error handling PullRequestEvent.")
 					}
 				}(p, h.PullRequestHandler)
 			}


Test patch:
diff --git a/pkg/git/v2/interactor_test.go b/pkg/git/v2/interactor_test.go
index 707bab89..b1811434 100644
--- a/pkg/git/v2/interactor_test.go
+++ b/pkg/git/v2/interactor_test.go
@@ -42,12 +42,12 @@ func TestInteractor_Clone(t *testing.T) {
 			dir:  "/else",
 			from: "/somewhere",
 			responses: map[string]execResponse{
-				"clone /somewhere /else": {
+				"clone --no-checkout --depth=1 /somewhere /else": {
 					out: []byte(`ok`),
 				},
 			},
 			expectedCalls: [][]string{
-				{"clone", "/somewhere", "/else"},
+				{"clone", "--no-checkout", "--depth=1", "/somewhere", "/else"},
 			},
 			expectedErr: false,
 		},
@@ -56,12 +56,12 @@ func TestInteractor_Clone(t *testing.T) {
 			dir:  "/else",
 			from: "/somewhere",
 			responses: map[string]execResponse{
-				"clone /somewhere /else": {
+				"clone --no-checkout --depth=1 /somewhere /else": {
 					err: errors.New("oops"),
 				},
 			},
 			expectedCalls: [][]string{
-				{"clone", "/somewhere", "/else"},
+				{"clone", "--no-checkout", "--depth=1", "/somewhere", "/else"},
 			},
 			expectedErr: true,
 		},
@@ -109,12 +109,12 @@ func TestInteractor_MirrorClone(t *testing.T) {
 				return "someone.com", nil
 			},
 			responses: map[string]execResponse{
-				"clone --mirror someone.com /else": {
+				"clone --mirror --depth=1 someone.com /else": {
 					out: []byte(`ok`),
 				},
 			},
 			expectedCalls: [][]string{
-				{"clone", "--mirror", "someone.com", "/else"},
+				{"clone", "--mirror", "--depth=1", "someone.com", "/else"},
 			},
 			expectedErr: false,
 		},
@@ -135,12 +135,12 @@ func TestInteractor_MirrorClone(t *testing.T) {
 				return "someone.com", nil
 			},
 			responses: map[string]execResponse{
-				"clone --mirror someone.com /else": {
+				"clone --mirror --depth=1 someone.com /else": {
 					err: errors.New("oops"),
 				},
 			},
 			expectedCalls: [][]string{
-				{"clone", "--mirror", "someone.com", "/else"},
+				{"clone", "--mirror", "--depth=1", "someone.com", "/else"},
 			},
 			expectedErr: true,
 		},
diff --git a/pkg/plugins/command_test.go b/pkg/plugins/command_test.go
index 6d2b69ab..9981637c 100644
--- a/pkg/plugins/command_test.go
+++ b/pkg/plugins/command_test.go
@@ -421,7 +421,7 @@ func TestCommandGetMatches(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			matches, err := tc.command.GetMatches(tc.content)
 			if err != nil {
-				t.Errorf("an error has occured %w", err)
+				t.Errorf("an error has occured %v", err)
 			} else {
 				if !reflect.DeepEqual(tc.expected, matches) {
 					t.Errorf("expected matches %q, but got %q", tc.expected, matches)