1// Copyright 2019 The SwiftShader Authors. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package git provides functions for interacting with Git. 16package git 17 18import ( 19 "encoding/hex" 20 "fmt" 21 "io/ioutil" 22 "net/url" 23 "os" 24 "os/exec" 25 "strings" 26 "time" 27 28 "../cause" 29 "../shell" 30) 31 32const ( 33 gitTimeout = time.Minute * 15 // timeout for a git operation 34) 35 36var exe string 37 38func init() { 39 path, err := exec.LookPath("git") 40 if err != nil { 41 panic(cause.Wrap(err, "Couldn't find path to git executable")) 42 } 43 exe = path 44} 45 46// Hash is a 20 byte, git object hash. 47type Hash [20]byte 48 49func (h Hash) String() string { return hex.EncodeToString(h[:]) } 50 51// ParseHash returns a Hash from a hexadecimal string. 52func ParseHash(s string) Hash { 53 b, _ := hex.DecodeString(s) 54 h := Hash{} 55 copy(h[:], b) 56 return h 57} 58 59// Add calls 'git add <file>'. 60func Add(wd, file string) error { 61 if err := shell.Shell(gitTimeout, exe, wd, "add", file); err != nil { 62 return cause.Wrap(err, "`git add %v` in working directory %v failed", file, wd) 63 } 64 return nil 65} 66 67// CommitFlags advanced flags for Commit 68type CommitFlags struct { 69 Name string // Used for author and committer 70 Email string // Used for author and committer 71} 72 73// Commit calls 'git commit -m <msg> --author <author>'. 74func Commit(wd, msg string, flags CommitFlags) error { 75 args := []string{} 76 if flags.Name != "" { 77 args = append(args, "-c", "user.name="+flags.Name) 78 } 79 if flags.Email != "" { 80 args = append(args, "-c", "user.email="+flags.Email) 81 } 82 args = append(args, "commit", "-m", msg) 83 return shell.Shell(gitTimeout, exe, wd, args...) 84} 85 86// PushFlags advanced flags for Commit 87type PushFlags struct { 88 Username string // Used for authentication when uploading 89 Password string // Used for authentication when uploading 90} 91 92// Push pushes the local branch to remote. 93func Push(wd, remote, localBranch, remoteBranch string, flags PushFlags) error { 94 args := []string{} 95 if flags.Username != "" { 96 f, err := ioutil.TempFile("", "regres-cookies.txt") 97 if err != nil { 98 return cause.Wrap(err, "Couldn't create cookie file") 99 } 100 defer f.Close() 101 defer os.Remove(f.Name()) 102 u, err := url.Parse(remote) 103 if err != nil { 104 return cause.Wrap(err, "Couldn't parse url '%v'", remote) 105 } 106 f.WriteString(fmt.Sprintf("%v FALSE / TRUE 2147483647 o %v=%v\n", u.Host, flags.Username, flags.Password)) 107 f.Close() 108 args = append(args, "-c", "http.cookiefile="+f.Name()) 109 } 110 args = append(args, "push", remote, localBranch+":"+remoteBranch) 111 return shell.Shell(gitTimeout, exe, wd, args...) 112} 113 114// CheckoutRemoteBranch performs a git fetch and checkout of the given branch into path. 115func CheckoutRemoteBranch(path, url string, branch string) error { 116 if err := os.MkdirAll(path, 0777); err != nil { 117 return cause.Wrap(err, "mkdir '"+path+"' failed") 118 } 119 120 for _, cmds := range [][]string{ 121 {"init"}, 122 {"remote", "add", "origin", url}, 123 {"fetch", "origin", "--depth=1", branch}, 124 {"checkout", branch}, 125 } { 126 if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil { 127 os.RemoveAll(path) 128 return err 129 } 130 } 131 132 return nil 133} 134 135// CheckoutRemoteCommit performs a git fetch and checkout of the given commit into path. 136func CheckoutRemoteCommit(path, url string, commit Hash) error { 137 if err := os.MkdirAll(path, 0777); err != nil { 138 return cause.Wrap(err, "mkdir '"+path+"' failed") 139 } 140 141 for _, cmds := range [][]string{ 142 {"init"}, 143 {"remote", "add", "origin", url}, 144 {"fetch", "origin", "--depth=1", commit.String()}, 145 {"checkout", commit.String()}, 146 } { 147 if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil { 148 os.RemoveAll(path) 149 return err 150 } 151 } 152 153 return nil 154} 155 156// CheckoutCommit performs a git checkout of the given commit. 157func CheckoutCommit(path string, commit Hash) error { 158 return shell.Shell(gitTimeout, exe, path, "checkout", commit.String()) 159} 160 161// Apply applys the patch file to the git repo at dir. 162func Apply(dir, patch string) error { 163 return shell.Shell(gitTimeout, exe, dir, "apply", patch) 164} 165 166// FetchRefHash returns the git hash of the given ref. 167func FetchRefHash(ref, url string) (Hash, error) { 168 out, err := shell.Exec(gitTimeout, exe, "", nil, "ls-remote", url, ref) 169 if err != nil { 170 return Hash{}, err 171 } 172 return ParseHash(string(out)), nil 173} 174 175type ChangeList struct { 176 Hash Hash 177 Date time.Time 178 Author string 179 Subject string 180 Description string 181} 182 183// Log returns the top count ChangeLists at HEAD. 184func Log(path string, count int) ([]ChangeList, error) { 185 return LogFrom(path, "HEAD", count) 186} 187 188// LogFrom returns the top count ChangeList starting from at. 189func LogFrom(path, at string, count int) ([]ChangeList, error) { 190 if at == "" { 191 at = "HEAD" 192 } 193 out, err := shell.Exec(gitTimeout, exe, "", nil, "log", at, "--pretty=format:"+prettyFormat, fmt.Sprintf("-%d", count), path) 194 if err != nil { 195 return nil, err 196 } 197 return parseLog(string(out)), nil 198} 199 200// Parent returns the parent ChangeList for cl. 201func Parent(cl ChangeList) (ChangeList, error) { 202 out, err := shell.Exec(gitTimeout, exe, "", nil, "log", "--pretty=format:"+prettyFormat, fmt.Sprintf("%v^", cl.Hash)) 203 if err != nil { 204 return ChangeList{}, err 205 } 206 cls := parseLog(string(out)) 207 if len(cls) == 0 { 208 return ChangeList{}, fmt.Errorf("Unexpected output") 209 } 210 return cls[0], nil 211} 212 213// HeadCL returns the HEAD ChangeList at the given commit/tag/branch. 214func HeadCL(path string) (ChangeList, error) { 215 cls, err := LogFrom(path, "HEAD", 1) 216 if err != nil { 217 return ChangeList{}, err 218 } 219 if len(cls) == 0 { 220 return ChangeList{}, fmt.Errorf("No commits found") 221 } 222 return cls[0], nil 223} 224 225// Show content of the file at path for the given commit/tag/branch. 226func Show(path, at string) ([]byte, error) { 227 return shell.Exec(gitTimeout, exe, "", nil, "show", at+":"+path) 228} 229 230const prettyFormat = "ǁ%Hǀ%cIǀ%an <%ae>ǀ%sǀ%b" 231 232func parseLog(str string) []ChangeList { 233 msgs := strings.Split(str, "ǁ") 234 cls := make([]ChangeList, 0, len(msgs)) 235 for _, s := range msgs { 236 if parts := strings.Split(s, "ǀ"); len(parts) == 5 { 237 cl := ChangeList{ 238 Hash: ParseHash(parts[0]), 239 Author: strings.TrimSpace(parts[2]), 240 Subject: strings.TrimSpace(parts[3]), 241 Description: strings.TrimSpace(parts[4]), 242 } 243 date, err := time.Parse(time.RFC3339, parts[1]) 244 if err != nil { 245 panic(err) 246 } 247 cl.Date = date 248 249 cls = append(cls, cl) 250 } 251 } 252 return cls 253} 254