github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/commit.go (about)

     1  package vcs
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/opentracing/opentracing-go"
     8  	"golang.org/x/sync/errgroup"
     9  
    10  	vcsv1 "github.com/grafana/pyroscope/api/gen/proto/go/vcs/v1"
    11  )
    12  
    13  const maxConcurrentRequests = 10
    14  
    15  type gitHubCommitGetter interface {
    16  	GetCommit(context.Context, string, string, string) (*vcsv1.CommitInfo, error)
    17  }
    18  
    19  // getCommits fetches multiple commits in parallel for a given repository.
    20  // It attempts to retrieve commits for all provided refs and returns:
    21  // 1. Successfully fetched commits
    22  // 2. Errors for failed fetches
    23  // 3. An overall error if no commits were successfully fetched
    24  // This function provides partial success behavior, returning any commits
    25  // that were successfully fetched along with errors for those that failed.
    26  func getCommits(ctx context.Context, client gitHubCommitGetter, owner, repo string, refs []string) ([]*vcsv1.CommitInfo, []error, error) {
    27  	sp, ctx := opentracing.StartSpanFromContext(ctx, "getCommits")
    28  	defer sp.Finish()
    29  	sp.SetTag("owner", owner)
    30  	sp.SetTag("repo", repo)
    31  	sp.SetTag("ref_count", len(refs))
    32  
    33  	type result struct {
    34  		commit *vcsv1.CommitInfo
    35  		err    error
    36  	}
    37  
    38  	resultsCh := make(chan result, maxConcurrentRequests)
    39  	g, ctx := errgroup.WithContext(ctx)
    40  	g.SetLimit(maxConcurrentRequests)
    41  
    42  	for _, ref := range refs {
    43  		ref := ref
    44  		g.Go(func() error {
    45  			commit, err := tryGetCommit(ctx, client, owner, repo, ref)
    46  			select {
    47  			case resultsCh <- result{commit, err}:
    48  			case <-ctx.Done():
    49  				return ctx.Err()
    50  			}
    51  			return nil
    52  		})
    53  	}
    54  
    55  	go func() {
    56  		_ = g.Wait() // ignore errors since they're handled in the `resultsCh`.
    57  		close(resultsCh)
    58  	}()
    59  
    60  	var validCommits []*vcsv1.CommitInfo
    61  	var failedFetches []error
    62  	for r := range resultsCh {
    63  		if r.err != nil {
    64  			failedFetches = append(failedFetches, r.err)
    65  		}
    66  		if r.commit != nil {
    67  			validCommits = append(validCommits, r.commit)
    68  		}
    69  	}
    70  
    71  	if len(validCommits) == 0 && len(failedFetches) > 0 {
    72  		return nil, failedFetches, fmt.Errorf("failed to fetch any commits")
    73  	}
    74  
    75  	return validCommits, failedFetches, nil
    76  }
    77  
    78  // tryGetCommit attempts to retrieve a commit using different ref formats (commit hash, branch, tag).
    79  // It tries each format in order and returns the first successful result.
    80  func tryGetCommit(ctx context.Context, client gitHubCommitGetter, owner, repo, ref string) (*vcsv1.CommitInfo, error) {
    81  	sp, ctx := opentracing.StartSpanFromContext(ctx, "tryGetCommit")
    82  	defer sp.Finish()
    83  	sp.SetTag("owner", owner)
    84  	sp.SetTag("repo", repo)
    85  	sp.SetTag("ref", ref)
    86  
    87  	refFormats := []string{
    88  		ref,            // Try as a commit hash
    89  		"heads/" + ref, // Try as a branch
    90  		"tags/" + ref,  // Try as a tag
    91  	}
    92  
    93  	var lastErr error
    94  	for _, format := range refFormats {
    95  		commit, err := client.GetCommit(ctx, owner, repo, format)
    96  		if err == nil {
    97  			return commit, nil
    98  		}
    99  
   100  		lastErr = err
   101  	}
   102  
   103  	return nil, lastErr
   104  }