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 "swiftshader.googlesource.com/SwiftShader/tests/regres/shell" 29) 30 31const ( 32 gitTimeout = time.Minute * 15 // timeout for a git operation 33) 34 35var exe string 36 37func init() { 38 path, err := exec.LookPath("git") 39 if err != nil { 40 panic(fmt.Errorf("failed to find path to git executable: %w", err)) 41 } 42 exe = path 43} 44 45// Hash is a 20 byte, git object hash. 46type Hash [20]byte 47 48func (h Hash) String() string { return hex.EncodeToString(h[:]) } 49 50// ParseHash returns a Hash from a hexadecimal string. 51func ParseHash(s string) Hash { 52 b, _ := hex.DecodeString(s) 53 h := Hash{} 54 copy(h[:], b) 55 return h 56} 57 58// Add calls 'git add <file>'. 59func Add(wd, file string) error { 60 if err := shell.Shell(gitTimeout, exe, wd, "add", file); err != nil { 61 return fmt.Errorf("`git add %v` in working directory %v failed: %w", file, wd, err) 62 } 63 return nil 64} 65 66// CommitFlags advanced flags for Commit 67type CommitFlags struct { 68 Name string // Used for author and committer 69 Email string // Used for author and committer 70} 71 72// Commit calls 'git commit -m <msg> --author <author>'. 73func Commit(wd, msg string, flags CommitFlags) error { 74 args := []string{} 75 if flags.Name != "" { 76 args = append(args, "-c", "user.name="+flags.Name) 77 } 78 if flags.Email != "" { 79 args = append(args, "-c", "user.email="+flags.Email) 80 } 81 args = append(args, "commit", "-m", msg) 82 return shell.Shell(gitTimeout, exe, wd, args...) 83} 84 85func InjectUserInHostUrl(userEmail string, url string) string { 86 if userEmail == "" { 87 return url 88 } 89 90 user := strings.Replace(userEmail, "@", "%40", 1) 91 return strings.Replace(url, "://", "://"+user+"@", 1) 92} 93 94// PushFlags advanced flags for Commit 95type PushFlags struct { 96 Username string // Used for authentication when uploading 97 Password string // Used for authentication when uploading 98} 99 100// Push pushes the local branch to remote. 101func Push(wd, remote, localBranch, remoteBranch string, flags PushFlags) error { 102 args := []string{} 103 if flags.Username != "" { 104 f, err := ioutil.TempFile("", "regres-cookies.txt") 105 if err != nil { 106 return fmt.Errorf("failed to create cookie file: %w", err) 107 } 108 defer f.Close() 109 defer os.Remove(f.Name()) 110 u, err := url.Parse(remote) 111 if err != nil { 112 return fmt.Errorf("failed to parse url '%v': %w", remote, err) 113 } 114 f.WriteString(fmt.Sprintf("%v FALSE / TRUE 2147483647 o %v=%v\n", u.Host, flags.Username, flags.Password)) 115 f.Close() 116 args = append(args, "-c", "http.cookiefile="+f.Name()) 117 118 remote = InjectUserInHostUrl(flags.Username, remote) 119 } 120 args = append(args, "push", remote, localBranch+":"+remoteBranch) 121 return shell.Shell(gitTimeout, exe, wd, args...) 122} 123 124// CheckoutRemoteBranch performs a git fetch and checkout of the given branch into path. 125func CheckoutRemoteBranch(path, url string, branch string, flags CommitFlags) error { 126 if err := os.MkdirAll(path, 0777); err != nil { 127 return fmt.Errorf("mkdir '"+path+"' failed: %w", err) 128 } 129 130 url = InjectUserInHostUrl(flags.Email, url) 131 132 for _, cmds := range [][]string{ 133 {"init"}, 134 {"config", "user.name", flags.Name}, 135 {"config", "user.email", flags.Email}, 136 {"remote", "add", "origin", url}, 137 {"fetch", "origin", "--depth=1", branch}, 138 {"checkout", branch}, 139 } { 140 if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil { 141 os.RemoveAll(path) 142 return err 143 } 144 } 145 146 return nil 147} 148 149// CheckoutRemoteCommit performs a git fetch and checkout of the given commit into path. 150func CheckoutRemoteCommit(path, url string, commit Hash, flags CommitFlags) error { 151 if err := os.MkdirAll(path, 0777); err != nil { 152 return fmt.Errorf("mkdir '"+path+"' failed: %w", err) 153 } 154 155 url = InjectUserInHostUrl(flags.Email, url) 156 157 for _, cmds := range [][]string{ 158 {"init"}, 159 {"config", "user.name", flags.Name}, 160 {"config", "user.email", flags.Email}, 161 {"remote", "add", "origin", url}, 162 {"fetch", "origin", "--depth=1", commit.String()}, 163 {"checkout", commit.String()}, 164 } { 165 if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil { 166 os.RemoveAll(path) 167 return err 168 } 169 } 170 171 return nil 172} 173 174// CheckoutCommit performs a git checkout of the given commit. 175func CheckoutCommit(path string, commit Hash) error { 176 return shell.Shell(gitTimeout, exe, path, "checkout", commit.String()) 177} 178 179// Apply applys the patch file to the git repo at dir. 180func Apply(dir, patch string) error { 181 return shell.Shell(gitTimeout, exe, dir, "apply", patch) 182} 183 184// FetchRefHash returns the git hash of the given ref. 185func FetchRefHash(ref, url string, userEmail string) (Hash, error) { 186 url = InjectUserInHostUrl(userEmail, url) 187 out, err := shell.Exec(gitTimeout, exe, "", nil, "", "ls-remote", url, ref) 188 if err != nil { 189 return Hash{}, err 190 } 191 return ParseHash(string(out)), nil 192} 193 194type ChangeList struct { 195 Hash Hash 196 Date time.Time 197 Author string 198 Subject string 199 Description string 200} 201 202// Log returns the top count ChangeLists at HEAD. 203func Log(path string, count int) ([]ChangeList, error) { 204 return LogFrom(path, "HEAD", count) 205} 206 207// LogFrom returns the top count ChangeList starting from at. 208func LogFrom(path, at string, count int) ([]ChangeList, error) { 209 if at == "" { 210 at = "HEAD" 211 } 212 out, err := shell.Exec(gitTimeout, exe, "", nil, "", "log", at, "--pretty=format:"+prettyFormat, fmt.Sprintf("-%d", count), path) 213 if err != nil { 214 return nil, err 215 } 216 return parseLog(string(out)), nil 217} 218 219// Parent returns the parent ChangeList for cl. 220func Parent(cl ChangeList) (ChangeList, error) { 221 out, err := shell.Exec(gitTimeout, exe, "", nil, "", "log", "--pretty=format:"+prettyFormat, fmt.Sprintf("%v^", cl.Hash)) 222 if err != nil { 223 return ChangeList{}, err 224 } 225 cls := parseLog(string(out)) 226 if len(cls) == 0 { 227 return ChangeList{}, fmt.Errorf("Unexpected output") 228 } 229 return cls[0], nil 230} 231 232// HeadCL returns the HEAD ChangeList at the given commit/tag/branch. 233func HeadCL(path string) (ChangeList, error) { 234 cls, err := LogFrom(path, "HEAD", 1) 235 if err != nil { 236 return ChangeList{}, err 237 } 238 if len(cls) == 0 { 239 return ChangeList{}, fmt.Errorf("No commits found") 240 } 241 return cls[0], nil 242} 243 244// Show content of the file at path for the given commit/tag/branch. 245func Show(path, at string) ([]byte, error) { 246 return shell.Exec(gitTimeout, exe, "", nil, "", "show", at+":"+path) 247} 248 249const prettyFormat = "ǁ%Hǀ%cIǀ%an <%ae>ǀ%sǀ%b" 250 251func parseLog(str string) []ChangeList { 252 msgs := strings.Split(str, "ǁ") 253 cls := make([]ChangeList, 0, len(msgs)) 254 for _, s := range msgs { 255 if parts := strings.Split(s, "ǀ"); len(parts) == 5 { 256 cl := ChangeList{ 257 Hash: ParseHash(parts[0]), 258 Author: strings.TrimSpace(parts[2]), 259 Subject: strings.TrimSpace(parts[3]), 260 Description: strings.TrimSpace(parts[4]), 261 } 262 date, err := time.Parse(time.RFC3339, parts[1]) 263 if err != nil { 264 panic(err) 265 } 266 cl.Date = date 267 268 cls = append(cls, cl) 269 } 270 } 271 return cls 272} 273