github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/git/git.go (about)

     1  /*
     2  Copyright 2017 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 git provides a client to plugins that can do git operations.
    18  package git
    19  
    20  import (
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/sirupsen/logrus"
    30  )
    31  
    32  const github = "https://github.com"
    33  
    34  // Client can clone repos. It keeps a local cache, so successive clones of the
    35  // same repo should be quick. Create with NewClient. Be sure to clean it up.
    36  type Client struct {
    37  	// Logger will be used to log git operations and must be set.
    38  	Logger *logrus.Entry
    39  
    40  	// dir is the location of the git cache.
    41  	dir string
    42  	// git is the path to the git binary.
    43  	git string
    44  	// base is the base path for git clone calls. For users it will be set to
    45  	// GitHub, but for tests set it to a directory with git repos.
    46  	base string
    47  
    48  	// The mutex protects repoLocks which protect individual repos. This is
    49  	// necessary because Clone calls for the same repo are racy. Rather than
    50  	// one lock for all repos, use a lock per repo.
    51  	// Lock with Client.lockRepo, unlock with Client.unlockRepo.
    52  	rlm       sync.Mutex
    53  	repoLocks map[string]*sync.Mutex
    54  }
    55  
    56  // Clean removes the local repo cache. The Client is unusable after calling.
    57  func (c *Client) Clean() error {
    58  	return os.RemoveAll(c.dir)
    59  }
    60  
    61  // NewClient returns a client that talks to GitHub. It will fail if git is not
    62  // in the PATH.
    63  func NewClient() (*Client, error) {
    64  	g, err := exec.LookPath("git")
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	t, err := ioutil.TempDir("", "git")
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	return &Client{
    73  		dir:       t,
    74  		git:       g,
    75  		base:      github,
    76  		repoLocks: make(map[string]*sync.Mutex),
    77  	}, nil
    78  }
    79  
    80  // SetRemote sets the remote for the client. This is not thread-safe, and is
    81  // useful for testing. The client will clone from remote/org/repo, and Repo
    82  // objects spun out of the client will also hit that path.
    83  func (c *Client) SetRemote(remote string) {
    84  	c.base = remote
    85  }
    86  
    87  func (c *Client) lockRepo(repo string) {
    88  	c.rlm.Lock()
    89  	if _, ok := c.repoLocks[repo]; !ok {
    90  		c.repoLocks[repo] = &sync.Mutex{}
    91  	}
    92  	m := c.repoLocks[repo]
    93  	c.rlm.Unlock()
    94  	m.Lock()
    95  }
    96  
    97  func (c *Client) unlockRepo(repo string) {
    98  	c.rlm.Lock()
    99  	defer c.rlm.Unlock()
   100  	c.repoLocks[repo].Unlock()
   101  }
   102  
   103  // Clone clones a repository. Pass the full repository name, such as
   104  // "kubernetes/test-infra" as the repo.
   105  // This function may take a long time if it is the first time cloning the repo.
   106  // In that case, it must do a full git mirror clone. For large repos, this can
   107  // take a while. Once that is done, it will do a git fetch instead of a clone,
   108  // which will usually take at most a few seconds.
   109  func (c *Client) Clone(repo string) (*Repo, error) {
   110  	c.lockRepo(repo)
   111  	defer c.unlockRepo(repo)
   112  
   113  	remote := c.base + "/" + repo
   114  	cache := filepath.Join(c.dir, repo) + ".git"
   115  	if _, err := os.Stat(cache); os.IsNotExist(err) {
   116  		// Cache miss, clone it now.
   117  		c.Logger.Infof("Cloning %s for the first time.", repo)
   118  		if err := os.Mkdir(filepath.Dir(cache), os.ModePerm); err != nil && !os.IsExist(err) {
   119  			return nil, err
   120  		}
   121  		if b, err := retryCmd(c.Logger, "", c.git, "clone", "--mirror", remote, cache); err != nil {
   122  			return nil, fmt.Errorf("git cache clone error: %v. output: %s", err, string(b))
   123  		}
   124  	} else if err != nil {
   125  		return nil, err
   126  	} else {
   127  		// Cache hit. Do a git fetch to keep updated.
   128  		c.Logger.Infof("Fetching %s.", repo)
   129  		if b, err := retryCmd(c.Logger, cache, c.git, "fetch"); err != nil {
   130  			return nil, fmt.Errorf("git fetch error: %v. output: %s", err, string(b))
   131  		}
   132  	}
   133  	t, err := ioutil.TempDir("", "git")
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	if b, err := exec.Command(c.git, "clone", cache, t).CombinedOutput(); err != nil {
   138  		return nil, fmt.Errorf("git repo clone error: %v. output: %s", err, string(b))
   139  	}
   140  	return &Repo{
   141  		Dir:    t,
   142  		logger: c.Logger,
   143  		git:    c.git,
   144  		base:   c.base,
   145  		repo:   repo,
   146  	}, nil
   147  }
   148  
   149  // Repo is a clone of a git repository. Create with Client.Clone, and don't
   150  // forget to clean it up after.
   151  type Repo struct {
   152  	// Dir is the location of the git repo.
   153  	Dir string
   154  
   155  	// git is the path to the git binary.
   156  	git string
   157  	// base is the base path for remote git fetch calls.
   158  	base string
   159  	// repo is the full repo name: "org/repo".
   160  	repo string
   161  
   162  	logger *logrus.Entry
   163  }
   164  
   165  // Clean deletes the repo. It is unusable after calling.
   166  func (r *Repo) Clean() error {
   167  	return os.RemoveAll(r.Dir)
   168  }
   169  
   170  func (r *Repo) gitCommand(arg ...string) *exec.Cmd {
   171  	cmd := exec.Command(r.git, arg...)
   172  	cmd.Dir = r.Dir
   173  	return cmd
   174  }
   175  
   176  // Checkout runs git checkout.
   177  func (r *Repo) Checkout(commitlike string) error {
   178  	r.logger.Infof("Checkout %s.", commitlike)
   179  	co := r.gitCommand("checkout", commitlike)
   180  	if b, err := co.CombinedOutput(); err != nil {
   181  		return fmt.Errorf("error checking out %s: %v. output: %s", commitlike, err, string(b))
   182  	}
   183  	return nil
   184  }
   185  
   186  // Merge attempts to merge commitlike into the current branch. It returns true
   187  // if the merge completes. It returns an error if the abort fails.
   188  func (r *Repo) Merge(commitlike string) (bool, error) {
   189  	r.logger.Infof("Merging %s.", commitlike)
   190  	co := r.gitCommand("merge", "--no-ff", "--no-stat", "-m merge", commitlike)
   191  	if b, err := co.CombinedOutput(); err == nil {
   192  		return true, nil
   193  	} else {
   194  		r.logger.WithError(err).Warningf("Merge failed with output: %s", string(b))
   195  	}
   196  	if b, err := r.gitCommand("merge", "--abort").CombinedOutput(); err != nil {
   197  		return false, fmt.Errorf("error aborting merge for commitlike %s: %v. output: %s", commitlike, err, string(b))
   198  	}
   199  	return false, nil
   200  }
   201  
   202  // CheckoutPullRequest does exactly that.
   203  func (r *Repo) CheckoutPullRequest(number int) error {
   204  	r.logger.Infof("Fetching and checking out %s#%d.", r.repo, number)
   205  	if b, err := retryCmd(r.logger, r.Dir, r.git, "fetch", r.base+"/"+r.repo, fmt.Sprintf("pull/%d/head:pull%d", number, number)); err != nil {
   206  		return fmt.Errorf("git fetch failed for PR %d: %v. output: %s", number, err, string(b))
   207  	}
   208  	co := r.gitCommand("checkout", fmt.Sprintf("pull%d", number))
   209  	if b, err := co.CombinedOutput(); err != nil {
   210  		return fmt.Errorf("git checkout failed for PR %d: %v. output: %s", number, err, string(b))
   211  	}
   212  	return nil
   213  }
   214  
   215  // Config runs git config.
   216  func (r *Repo) Config(key, value string) error {
   217  	r.logger.Infof("Running git config %s %s", key, value)
   218  	if b, err := r.gitCommand("config", key, value).CombinedOutput(); err != nil {
   219  		return fmt.Errorf("git config %s %s failed: %v. output: %s", key, value, err, string(b))
   220  	}
   221  	return nil
   222  }
   223  
   224  // retryCmd will retry the command a few times with backoff. Use this for any
   225  // commands that will be talking to GitHub, such as clones or fetches.
   226  func retryCmd(l *logrus.Entry, dir, cmd string, arg ...string) ([]byte, error) {
   227  	var b []byte
   228  	var err error
   229  	sleepyTime := time.Second
   230  	for i := 0; i < 3; i++ {
   231  		cmd := exec.Command(cmd, arg...)
   232  		cmd.Dir = dir
   233  		b, err = cmd.CombinedOutput()
   234  		if err != nil {
   235  			l.Warningf("Running %s %v returned error %v with output %s.", cmd, arg, err, string(b))
   236  			time.Sleep(sleepyTime)
   237  			sleepyTime *= 2
   238  			continue
   239  		}
   240  		break
   241  	}
   242  	return b, err
   243  }