github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/pod-utils/clone/clone.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package clone
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os/exec"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/sirupsen/logrus"
    28  	"k8s.io/test-infra/prow/kube"
    29  )
    30  
    31  // Run clones the refs under the prescribed directory and optionally
    32  // configures the git username and email in the repository as well.
    33  func Run(refs kube.Refs, dir, gitUserName, gitUserEmail, cookiePath string, env []string) Record {
    34  	logrus.WithFields(logrus.Fields{"refs": refs}).Info("Cloning refs")
    35  	record := Record{Refs: refs}
    36  
    37  	// This function runs the provided commands in order, logging them as they run,
    38  	// aborting early and returning if any command fails.
    39  	runCommands := func(commands []cloneCommand) error {
    40  		for _, command := range commands {
    41  			formattedCommand, output, err := command.run()
    42  			logrus.WithFields(logrus.Fields{"command": formattedCommand, "output": output, "error": err}).Info("Ran command")
    43  			message := ""
    44  			if err != nil {
    45  				message = err.Error()
    46  				record.Failed = true
    47  			}
    48  			record.Commands = append(record.Commands, Command{Command: formattedCommand, Output: output, Error: message})
    49  			if err != nil {
    50  				return err
    51  			}
    52  		}
    53  		return nil
    54  	}
    55  
    56  	g := gitCtxForRefs(refs, dir, env)
    57  	if err := runCommands(g.commandsForBaseRef(refs, gitUserName, gitUserEmail, cookiePath)); err != nil {
    58  		return record
    59  	}
    60  	timestamp, err := g.gitHeadTimestamp()
    61  	if err != nil {
    62  		timestamp = int(time.Now().Unix())
    63  	}
    64  	if err := runCommands(g.commandsForPullRefs(refs, timestamp)); err != nil {
    65  		return record
    66  	}
    67  	return record
    68  }
    69  
    70  // PathForRefs determines the full path to where
    71  // refs should be cloned
    72  func PathForRefs(baseDir string, refs kube.Refs) string {
    73  	var clonePath string
    74  	if refs.PathAlias != "" {
    75  		clonePath = refs.PathAlias
    76  	} else {
    77  		clonePath = fmt.Sprintf("github.com/%s/%s", refs.Org, refs.Repo)
    78  	}
    79  	return fmt.Sprintf("%s/src/%s", baseDir, clonePath)
    80  }
    81  
    82  // gitCtx collects a few common values needed for all git commands.
    83  type gitCtx struct {
    84  	cloneDir      string
    85  	env           []string
    86  	repositoryURI string
    87  }
    88  
    89  // gitCtxForRefs creates a gitCtx based on the provide refs and baseDir.
    90  func gitCtxForRefs(refs kube.Refs, baseDir string, env []string) gitCtx {
    91  	g := gitCtx{
    92  		cloneDir:      PathForRefs(baseDir, refs),
    93  		env:           env,
    94  		repositoryURI: fmt.Sprintf("https://github.com/%s/%s.git", refs.Org, refs.Repo),
    95  	}
    96  	if refs.CloneURI != "" {
    97  		g.repositoryURI = refs.CloneURI
    98  	}
    99  	return g
   100  }
   101  
   102  func (g *gitCtx) gitCommand(args ...string) cloneCommand {
   103  	return cloneCommand{dir: g.cloneDir, env: g.env, command: "git", args: args}
   104  }
   105  
   106  // commandsForBaseRef returns the list of commands needed to initialize and
   107  // configure a local git directory, as well as fetch and check out the provided
   108  // base ref.
   109  func (g *gitCtx) commandsForBaseRef(refs kube.Refs, gitUserName, gitUserEmail, cookiePath string) []cloneCommand {
   110  	commands := []cloneCommand{{dir: "/", env: g.env, command: "mkdir", args: []string{"-p", g.cloneDir}}}
   111  
   112  	commands = append(commands, g.gitCommand("init"))
   113  	if gitUserName != "" {
   114  		commands = append(commands, g.gitCommand("config", "user.name", gitUserName))
   115  	}
   116  	if gitUserEmail != "" {
   117  		commands = append(commands, g.gitCommand("config", "user.email", gitUserEmail))
   118  	}
   119  	if cookiePath != "" {
   120  		commands = append(commands, g.gitCommand("config", "http.cookiefile", cookiePath))
   121  	}
   122  	commands = append(commands, g.gitCommand("fetch", g.repositoryURI, "--tags", "--prune"))
   123  	commands = append(commands, g.gitCommand("fetch", g.repositoryURI, refs.BaseRef))
   124  
   125  	var target string
   126  	if refs.BaseSHA != "" {
   127  		target = refs.BaseSHA
   128  	} else {
   129  		target = "FETCH_HEAD"
   130  	}
   131  	// we need to be "on" the target branch after the sync
   132  	// so we need to set the branch to point to the base ref,
   133  	// but we cannot update a branch we are on, so in case we
   134  	// are on the branch we are syncing, we check out the SHA
   135  	// first and reset the branch second, then check out the
   136  	// branch we just reset to be in the correct final state
   137  	commands = append(commands, g.gitCommand("checkout", target))
   138  	commands = append(commands, g.gitCommand("branch", "--force", refs.BaseRef, target))
   139  	commands = append(commands, g.gitCommand("checkout", refs.BaseRef))
   140  
   141  	return commands
   142  }
   143  
   144  // gitHeadTimestamp returns the timestamp of the HEAD commit as seconds from the
   145  // UNIX epoch. If unable to read the timestamp for any reason (such as missing
   146  // the git, or not using a git repo), it returns 0 and an error.
   147  func (g *gitCtx) gitHeadTimestamp() (int, error) {
   148  	gitShowCommand := g.gitCommand("show", "-s", "--format=format:%ct", "HEAD")
   149  	_, gitOutput, err := gitShowCommand.run()
   150  	if err != nil {
   151  		logrus.WithError(err).Debug("Could not obtain timestamp of git HEAD")
   152  		return 0, err
   153  	}
   154  	timestamp, convErr := strconv.Atoi(string(gitOutput))
   155  	if convErr != nil {
   156  		logrus.WithError(convErr).Errorf("Failed to parse timestamp %q", gitOutput)
   157  		return 0, convErr
   158  	}
   159  	return timestamp, nil
   160  }
   161  
   162  // gitTimestampEnvs returns the list of environment variables needed to override
   163  // git's author and commit timestamps when creating new commits.
   164  func gitTimestampEnvs(timestamp int) []string {
   165  	return []string{
   166  		fmt.Sprintf("GIT_AUTHOR_DATE=%d", timestamp),
   167  		fmt.Sprintf("GIT_COMMITTER_DATE=%d", timestamp),
   168  	}
   169  }
   170  
   171  // commandsForPullRefs returns the list of commands needed to fetch and
   172  // merge any pull refs as well as submodules. These commands should be run only
   173  // after the commands provided by commandsForBaseRef have been run
   174  // successfully.
   175  // Each merge commit will be created at sequential seconds after fakeTimestamp.
   176  // It's recommended that fakeTimestamp be set to the timestamp of the base ref.
   177  // This enables reproducible timestamps and git tree digests every time the same
   178  // set of base and pull refs are used.
   179  func (g *gitCtx) commandsForPullRefs(refs kube.Refs, fakeTimestamp int) []cloneCommand {
   180  	var commands []cloneCommand
   181  	for _, prRef := range refs.Pulls {
   182  		ref := fmt.Sprintf("pull/%d/head", prRef.Number)
   183  		if prRef.Ref != "" {
   184  			ref = prRef.Ref
   185  		}
   186  		commands = append(commands, g.gitCommand("fetch", g.repositoryURI, ref))
   187  		var prCheckout string
   188  		if prRef.SHA != "" {
   189  			prCheckout = prRef.SHA
   190  		} else {
   191  			prCheckout = "FETCH_HEAD"
   192  		}
   193  		fakeTimestamp++
   194  		gitMergeCommand := g.gitCommand("merge", "--no-ff", prCheckout)
   195  		gitMergeCommand.env = append(gitMergeCommand.env, gitTimestampEnvs(fakeTimestamp)...)
   196  		commands = append(commands, gitMergeCommand)
   197  	}
   198  
   199  	// unless the user specifically asks us not to, init submodules
   200  	if !refs.SkipSubmodules {
   201  		commands = append(commands, g.gitCommand("submodule", "update", "--init", "--recursive"))
   202  	}
   203  
   204  	return commands
   205  }
   206  
   207  type cloneCommand struct {
   208  	dir     string
   209  	env     []string
   210  	command string
   211  	args    []string
   212  }
   213  
   214  func (c *cloneCommand) run() (string, string, error) {
   215  	output := bytes.Buffer{}
   216  	cmd := exec.Command(c.command, c.args...)
   217  	cmd.Dir = c.dir
   218  	cmd.Env = append(cmd.Env, c.env...)
   219  	cmd.Stdout = &output
   220  	cmd.Stderr = &output
   221  	err := cmd.Run()
   222  	return strings.Join(append([]string{c.command}, c.args...), " "), output.String(), err
   223  }
   224  
   225  func (c *cloneCommand) String() string {
   226  	return fmt.Sprintf("PWD=%s %s %s %s", c.dir, strings.Join(c.env, " "), c.command, strings.Join(c.env, " "))
   227  }