| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package gitiles |
| |
| import ( |
| "context" |
| "fmt" |
| "net/http" |
| |
| "go.chromium.org/luci/common/api/gitiles" |
| "go.chromium.org/luci/common/proto/git" |
| gitilespb "go.chromium.org/luci/common/proto/gitiles" |
| "go.fuchsia.dev/infra/rpcutil" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| ) |
| |
| // noSuchCommittishError will be returned from a function that gets a NotFound |
| // response from Gitiles when trying to look up a specific ref or commit. |
| type noSuchCommittishError struct { |
| remote string |
| committish string |
| } |
| |
| func (e noSuchCommittishError) Error() string { |
| return fmt.Sprintf("%q does not exist in repository %s", e.committish, e.remote) |
| } |
| |
| func (e noSuchCommittishError) IsInfraFailure() bool { |
| return false |
| } |
| |
| // Client provides utilities for interacting with a Gitiles project. |
| type Client struct { |
| client gitilespb.GitilesClient |
| project string |
| host string |
| } |
| |
| // NewClient returns a gitilesClientWrapper for a host and project. |
| func NewClient(host, project string, httpClient *http.Client) (*Client, error) { |
| client, err := gitiles.NewRESTClient(httpClient, host, true) |
| if err != nil { |
| return nil, err |
| } |
| return &Client{client: client, project: project, host: host}, nil |
| } |
| |
| // LatestCommit resolves a ref to a revision. |
| func (c *Client) LatestCommit(ctx context.Context, ref string) (string, error) { |
| log, err := c.Log(ctx, ref, 1) |
| if err != nil { |
| return "", err |
| } |
| return log[0].GetId(), nil |
| } |
| |
| // Log returns a slice of commits of length `numCommits` starting from `committish`. |
| func (c *Client) Log(ctx context.Context, committish string, numCommits int32) ([]*git.Commit, error) { |
| url := fmt.Sprintf("%s/%s/+/%s", c.host, c.project, committish) |
| req := &gitilespb.LogRequest{ |
| Project: c.project, |
| Committish: committish, |
| PageSize: numCommits, |
| } |
| var resp *gitilespb.LogResponse |
| if err := rpcutil.RequestWithRetries(ctx, func() error { |
| var err error |
| if resp, err = c.client.Log(ctx, req); err != nil { |
| if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { |
| return noSuchCommittishError{ |
| remote: fmt.Sprintf("%s/%s", c.host, c.project), |
| committish: committish, |
| } |
| } |
| return err |
| } |
| return nil |
| // Insufficient Gitiles quota may be a transient error. |
| // 404s may result from transient Gerrit backend inconsistencies. |
| }, codes.ResourceExhausted, codes.NotFound); err != nil { |
| return nil, fmt.Errorf("failed to get log for %s: %w", url, err) |
| } |
| |
| if len(resp.Log) == 0 { |
| return nil, fmt.Errorf("empty log for %s", url) |
| } |
| return resp.Log, nil |
| } |
| |
| // DownloadFile returns the contents of the file at `path` and `ref`. |
| func (c *Client) DownloadFile(ctx context.Context, path, ref string) (string, error) { |
| resp, err := c.client.DownloadFile(ctx, &gitilespb.DownloadFileRequest{ |
| Project: c.project, |
| Committish: ref, |
| Path: path, |
| }) |
| if err != nil { |
| return "", fmt.Errorf("failed to download %s: %w", path, err) |
| } |
| return resp.Contents, nil |
| } |