| // Copyright 2019 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 checkout |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "net/url" |
| "strings" |
| |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| "google.golang.org/protobuf/proto" |
| |
| "go.fuchsia.dev/infra/execution" |
| ) |
| |
| // strategy checks out a git repository according to a Buildbucket build input. |
| type strategy interface { |
| Checkout(ctx context.Context, e *execution.Executor) error |
| Equal(other strategy) bool |
| } |
| |
| // newStrategy selects a strategy for the given git repo URL and build input. input is the |
| // current Buildbucket build input. repoURL is the repository containing test inputs. This |
| // ensures that the patched versions of test inputs are used if they're modified, or |
| // fetched from an expected revision if not. |
| // |
| // If the build input indicates that the repo at repoURL was modified: |
| // * checkout from patchset if the build input has a Gerrit change. |
| // * checkout from a commit if the build input has Gitiles commit. |
| // |
| // If the build input indicates otherwise: |
| // * error if there is no Gitiles commit or Gerrit change, to avoid masking the bug |
| // that there is no build input (nothing to test). |
| // * otherwise checkout at defaultRevision, which can be either a ref (e.g. |
| // refs/heads/main) or a commit hash. |
| func newStrategy(input *buildbucketpb.Build_Input, repoURL url.URL, defaultRevision string) (strategy, error) { |
| if len(input.GerritChanges) == 0 && input.GitilesCommit == nil { |
| return nil, errors.New("build input has no changes") |
| } |
| |
| if len(input.GerritChanges) > 0 { |
| change := input.GerritChanges[0] |
| changeURL := url.URL{ |
| Scheme: repoURL.Scheme, // gerrit changes have no scheme. |
| Host: RemoveCodeReviewSuffix(change.Host), |
| Path: change.Project, |
| } |
| if changeURL.String() == repoURL.String() { |
| return checkoutChange{change: change, parent: input.GitilesCommit}, nil |
| } |
| } else if input.GitilesCommit != nil { |
| commit := input.GitilesCommit |
| commitURL := url.URL{ |
| Scheme: repoURL.Scheme, // gitiles commits have no scheme. |
| Host: commit.Host, |
| Path: commit.Project, |
| } |
| if commitURL.String() == repoURL.String() { |
| return checkoutCommit{commit: input.GitilesCommit}, nil |
| } |
| } |
| |
| // The current build was not triggered by a change to the specified |
| // repository, so we'll checkout at the default revision. |
| commit := &buildbucketpb.GitilesCommit{ |
| Host: repoURL.Host, |
| Project: strings.TrimLeft(repoURL.Path, "/"), |
| Id: defaultRevision, |
| } |
| return checkoutCommit{commit}, nil |
| } |
| |
| // checks out from a Gerrit change by rebasing that change on top of a Gitiles commit. |
| type checkoutChange struct { |
| change *buildbucketpb.GerritChange |
| parent *buildbucketpb.GitilesCommit |
| } |
| |
| func (c checkoutChange) Equal(o strategy) bool { |
| switch other := o.(type) { |
| case checkoutChange: |
| return proto.Equal(other.parent, c.parent) && proto.Equal(other.change, c.change) |
| default: |
| return false |
| } |
| } |
| |
| func (c checkoutChange) String() string { |
| return fmt.Sprintf("change: %s parent: %s", c.change, c.parent) |
| } |
| |
| func (c checkoutChange) Checkout(ctx context.Context, executor *execution.Executor) error { |
| host := RemoveCodeReviewSuffix(c.change.Host) |
| url := fmt.Sprintf("https://%s/%s", host, c.change.Project) |
| ref := GitilesChangeRef(c.change) |
| |
| // If rebase is not necessary, we only need to fetch depth 1. |
| var fetchOpt string |
| if c.parent == nil { |
| fetchOpt = "--depth=1" |
| } else { |
| fetchOpt = "--tags" |
| } |
| |
| cmds := []execution.Command{ |
| // Checkout the patch. |
| {Args: []string{git, "init", "--quiet"}}, |
| {Args: []string{git, "remote", "add", "origin", url}}, |
| {Args: []string{git, "fetch", fetchOpt, "origin", ref}}, |
| {Args: []string{git, "checkout", "--force", "FETCH_HEAD"}}, |
| } |
| if c.parent != nil { |
| cmds = append(cmds, []execution.Command{ |
| // Rebase on top of parent. |
| {Args: []string{git, "fetch", "origin", c.parent.Id}}, |
| // Rebase failures are generally user-caused, e.g. because the user |
| // is trying to rebase a change that has a merge conflict with the |
| // parent that needs to be manually fixed. |
| {Args: []string{git, "rebase", "FETCH_HEAD"}, UserCausedError: true}, |
| }...) |
| } |
| return executor.ExecAll(ctx, cmds) |
| } |
| |
| // checks out from a Gitiles commit. |
| type checkoutCommit struct { |
| commit *buildbucketpb.GitilesCommit |
| } |
| |
| func (c checkoutCommit) String() string { |
| return fmt.Sprintf("commit: %s", c.commit) |
| } |
| |
| func (c checkoutCommit) Equal(o strategy) bool { |
| switch other := o.(type) { |
| case checkoutCommit: |
| return proto.Equal(other.commit, c.commit) |
| default: |
| return false |
| } |
| } |
| |
| func (c checkoutCommit) Checkout(ctx context.Context, executor *execution.Executor) error { |
| url := fmt.Sprintf("https://%s/%s", c.commit.Host, c.commit.Project) |
| return executor.ExecAll(ctx, []execution.Command{ |
| {Args: []string{git, "init", "--quiet"}}, |
| {Args: []string{git, "remote", "add", "origin", url}}, |
| {Args: []string{git, "fetch", "--depth=1", "origin", c.commit.Id}}, |
| {Args: []string{git, "checkout", "FETCH_HEAD"}}, |
| }) |
| } |
| |
| // Converts foo-review.googlesource.com to foo.googlesource.com |
| func RemoveCodeReviewSuffix(host string) string { |
| return strings.ReplaceAll(host, "-review.googlesource.com", ".googlesource.com") |
| } |
| |
| // Returns the Gitiles ref for a gerrit changeNumber. The ref has the form xx/yyyy/zz |
| // where xx is `yyyy modulo 100` (always 2 digits), and zz is the patchset number. |
| func GitilesChangeRef(change *buildbucketpb.GerritChange) string { |
| changeno := change.Change |
| return fmt.Sprintf("refs/changes/%02d/%d/%d", changeno%100, changeno, change.Patchset) |
| } |