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 }