go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/checkout/strategy.go (about)

     1  // Copyright 2019 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package checkout
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"net/url"
    12  	"strings"
    13  
    14  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    15  	"google.golang.org/protobuf/proto"
    16  
    17  	"go.fuchsia.dev/infra/execution"
    18  )
    19  
    20  // strategy checks out a git repository according to a Buildbucket build input.
    21  type strategy interface {
    22  	Checkout(ctx context.Context, e *execution.Executor) error
    23  	Equal(other strategy) bool
    24  }
    25  
    26  // newStrategy selects a strategy for the given git repo URL and build input. input is the
    27  // current Buildbucket build input. repoURL is the repository containing test inputs. This
    28  // ensures that the patched versions of test inputs are used if they're modified, or
    29  // fetched from an expected revision if not.
    30  //
    31  //	If the build input indicates that the repo at repoURL was modified:
    32  //	  * checkout from patchset if the build input has a Gerrit change.
    33  //	  * checkout from a commit if the build input has Gitiles commit.
    34  //
    35  //	If the build input indicates otherwise:
    36  //	  * error if there is no Gitiles commit or Gerrit change, to avoid masking the bug
    37  //	    that there is no build input (nothing to test).
    38  //	  * otherwise checkout at defaultRevision, which can be either a ref (e.g.
    39  //	    refs/heads/main) or a commit hash.
    40  func newStrategy(input *buildbucketpb.Build_Input, repoURL url.URL, defaultRevision string) (strategy, error) {
    41  	if len(input.GerritChanges) == 0 && input.GitilesCommit == nil {
    42  		return nil, errors.New("build input has no changes")
    43  	}
    44  
    45  	if len(input.GerritChanges) > 0 {
    46  		change := input.GerritChanges[0]
    47  		changeURL := url.URL{
    48  			Scheme: repoURL.Scheme, // gerrit changes have no scheme.
    49  			Host:   RemoveCodeReviewSuffix(change.Host),
    50  			Path:   change.Project,
    51  		}
    52  		if changeURL.String() == repoURL.String() {
    53  			return checkoutChange{change: change, parent: input.GitilesCommit}, nil
    54  		}
    55  	} else if input.GitilesCommit != nil {
    56  		commit := input.GitilesCommit
    57  		commitURL := url.URL{
    58  			Scheme: repoURL.Scheme, // gitiles commits have no scheme.
    59  			Host:   commit.Host,
    60  			Path:   commit.Project,
    61  		}
    62  		if commitURL.String() == repoURL.String() {
    63  			return checkoutCommit{commit: input.GitilesCommit}, nil
    64  		}
    65  	}
    66  
    67  	// The current build was not triggered by a change to the specified
    68  	// repository, so we'll checkout at the default revision.
    69  	commit := &buildbucketpb.GitilesCommit{
    70  		Host:    repoURL.Host,
    71  		Project: strings.TrimLeft(repoURL.Path, "/"),
    72  		Id:      defaultRevision,
    73  	}
    74  	return checkoutCommit{commit}, nil
    75  }
    76  
    77  // checks out from a Gerrit change by rebasing that change on top of a Gitiles commit.
    78  type checkoutChange struct {
    79  	change *buildbucketpb.GerritChange
    80  	parent *buildbucketpb.GitilesCommit
    81  }
    82  
    83  func (c checkoutChange) Equal(o strategy) bool {
    84  	switch other := o.(type) {
    85  	case checkoutChange:
    86  		return proto.Equal(other.parent, c.parent) && proto.Equal(other.change, c.change)
    87  	default:
    88  		return false
    89  	}
    90  }
    91  
    92  func (c checkoutChange) String() string {
    93  	return fmt.Sprintf("change: %s parent: %s", c.change, c.parent)
    94  }
    95  
    96  func (c checkoutChange) Checkout(ctx context.Context, executor *execution.Executor) error {
    97  	host := RemoveCodeReviewSuffix(c.change.Host)
    98  	url := fmt.Sprintf("https://%s/%s", host, c.change.Project)
    99  	ref := GitilesChangeRef(c.change)
   100  
   101  	// If rebase is not necessary, we only need to fetch depth 1.
   102  	var fetchOpt string
   103  	if c.parent == nil {
   104  		fetchOpt = "--depth=1"
   105  	} else {
   106  		fetchOpt = "--tags"
   107  	}
   108  
   109  	cmds := []execution.Command{
   110  		// Checkout the patch.
   111  		{Args: []string{git, "init", "--quiet"}},
   112  		{Args: []string{git, "remote", "add", "origin", url}},
   113  		{Args: []string{git, "fetch", fetchOpt, "origin", ref}},
   114  		{Args: []string{git, "checkout", "--force", "FETCH_HEAD"}},
   115  	}
   116  	if c.parent != nil {
   117  		cmds = append(cmds, []execution.Command{
   118  			// Rebase on top of parent.
   119  			{Args: []string{git, "fetch", "origin", c.parent.Id}},
   120  			// Rebase failures are generally user-caused, e.g. because the user
   121  			// is trying to rebase a change that has a merge conflict with the
   122  			// parent that needs to be manually fixed.
   123  			{Args: []string{git, "rebase", "FETCH_HEAD"}, UserCausedError: true},
   124  		}...)
   125  	}
   126  	return executor.ExecAll(ctx, cmds)
   127  }
   128  
   129  // checks out from a Gitiles commit.
   130  type checkoutCommit struct {
   131  	commit *buildbucketpb.GitilesCommit
   132  }
   133  
   134  func (c checkoutCommit) String() string {
   135  	return fmt.Sprintf("commit: %s", c.commit)
   136  }
   137  
   138  func (c checkoutCommit) Equal(o strategy) bool {
   139  	switch other := o.(type) {
   140  	case checkoutCommit:
   141  		return proto.Equal(other.commit, c.commit)
   142  	default:
   143  		return false
   144  	}
   145  }
   146  
   147  func (c checkoutCommit) Checkout(ctx context.Context, executor *execution.Executor) error {
   148  	url := fmt.Sprintf("https://%s/%s", c.commit.Host, c.commit.Project)
   149  	return executor.ExecAll(ctx, []execution.Command{
   150  		{Args: []string{git, "init", "--quiet"}},
   151  		{Args: []string{git, "remote", "add", "origin", url}},
   152  		{Args: []string{git, "fetch", "--depth=1", "origin", c.commit.Id}},
   153  		{Args: []string{git, "checkout", "FETCH_HEAD"}},
   154  	})
   155  }
   156  
   157  // Converts foo-review.googlesource.com to foo.googlesource.com
   158  func RemoveCodeReviewSuffix(host string) string {
   159  	return strings.ReplaceAll(host, "-review.googlesource.com", ".googlesource.com")
   160  }
   161  
   162  // Returns the Gitiles ref for a gerrit changeNumber. The ref has the form xx/yyyy/zz
   163  // where xx is `yyyy modulo 100` (always 2 digits), and zz is the patchset number.
   164  func GitilesChangeRef(change *buildbucketpb.GerritChange) string {
   165  	changeno := change.Change
   166  	return fmt.Sprintf("refs/changes/%02d/%d/%d", changeno%100, changeno, change.Patchset)
   167  }